kopia lustrzana https://github.com/NanoVNA-Saver/nanovna-saver
used black for reformatting/lintig
rodzic
b0110002ec
commit
50b540a832
|
@ -28,9 +28,10 @@ try:
|
|||
from NanoVNASaver.__main__ import main
|
||||
except ModuleNotFoundError:
|
||||
import sys
|
||||
sys.path.append('src')
|
||||
|
||||
sys.path.append("src")
|
||||
from NanoVNASaver.__main__ import main
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
|
@ -20,7 +20,8 @@
|
|||
VERSION = "0.6.0-pre"
|
||||
VERSION_URL = (
|
||||
"https://raw.githubusercontent.com/"
|
||||
"NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py")
|
||||
"NanoVNA-Saver/nanovna-saver/master/NanoVNASaver/About.py"
|
||||
)
|
||||
|
||||
INFO_URL = "https://github.com/NanoVNA-Saver/nanovna-saver"
|
||||
INFO = f"""NanoVNASaver {VERSION}
|
||||
|
|
|
@ -35,6 +35,7 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
Useful for tuning magloop.
|
||||
|
||||
"""
|
||||
|
||||
max_dips_shown = 1
|
||||
|
||||
vswr_bandwith_value = 2.56 # -3 dB ?!?
|
||||
|
@ -56,12 +57,17 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
if self.min_freq is None:
|
||||
self.min_freq = new_start
|
||||
self.max_freq = new_end
|
||||
logger.debug("setting hard limits to %s - %s",
|
||||
self.min_freq, self.max_freq)
|
||||
logger.debug(
|
||||
"setting hard limits to %s - %s", self.min_freq, self.max_freq
|
||||
)
|
||||
|
||||
if len(self.minimums) > 1:
|
||||
self.layout.addRow("", QtWidgets.QLabel(
|
||||
"Multiple minimums, not magloop or try to lower VSWR limit"))
|
||||
self.layout.addRow(
|
||||
"",
|
||||
QtWidgets.QLabel(
|
||||
"Multiple minimums, not magloop or try to lower VSWR limit"
|
||||
),
|
||||
)
|
||||
return
|
||||
|
||||
if len(self.minimums) == 1:
|
||||
|
@ -73,22 +79,25 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
logger.debug(" Zoom to %s-%s", new_start, new_end)
|
||||
|
||||
elif self.vswr_limit_value == self.vswr_bandwith_value:
|
||||
Q = self.app.data.s11[lowest].freq / \
|
||||
(self.app.data.s11[end].freq -
|
||||
self.app.data.s11[start].freq)
|
||||
Q = self.app.data.s11[lowest].freq / (
|
||||
self.app.data.s11[end].freq - self.app.data.s11[start].freq
|
||||
)
|
||||
self.layout.addRow("Q", QtWidgets.QLabel(f"{int(Q)}"))
|
||||
new_start = self.app.data.s11[start].freq - self.bandwith
|
||||
new_end = self.app.data.s11[end].freq + self.bandwith
|
||||
logger.debug("Single Spot, new scan on %s-%s",
|
||||
new_start, new_end)
|
||||
logger.debug(
|
||||
"Single Spot, new scan on %s-%s", new_start, new_end
|
||||
)
|
||||
|
||||
if self.vswr_limit_value > self.vswr_bandwith_value:
|
||||
self.vswr_limit_value = max(
|
||||
self.vswr_bandwith_value, self.vswr_limit_value - 1)
|
||||
self.vswr_bandwith_value, self.vswr_limit_value - 1
|
||||
)
|
||||
self.input_vswr_limit.setValue(self.vswr_limit_value)
|
||||
logger.debug(
|
||||
"found higher minimum, lowering vswr search to %s",
|
||||
self.vswr_limit_value)
|
||||
self.vswr_limit_value,
|
||||
)
|
||||
else:
|
||||
new_start = new_start - 5 * self.bandwith
|
||||
new_end = new_end + 5 * self.bandwith
|
||||
|
@ -100,14 +109,17 @@ class MagLoopAnalysis(VSWRAnalysis):
|
|||
self.input_vswr_limit.setValue(self.vswr_limit_value)
|
||||
logger.debug(
|
||||
"no minimum found, looking for higher value %s",
|
||||
self.vswr_limit_value)
|
||||
self.vswr_limit_value,
|
||||
)
|
||||
|
||||
new_start = max(self.min_freq, new_start)
|
||||
new_end = min(self.max_freq, new_end)
|
||||
logger.debug("next search will be %s - %s for vswr %s",
|
||||
new_start,
|
||||
new_end,
|
||||
self.vswr_limit_value)
|
||||
logger.debug(
|
||||
"next search will be %s - %s for vswr %s",
|
||||
new_start,
|
||||
new_end,
|
||||
self.vswr_limit_value,
|
||||
)
|
||||
|
||||
self.app.sweep_control.set_start(new_start)
|
||||
self.app.sweep_control.set_end(new_end)
|
||||
|
|
|
@ -33,42 +33,52 @@ class BandPassAnalysis(Analysis):
|
|||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
for label in ('octave_l', 'octave_r', 'decade_l', 'decade_r',
|
||||
'freq_center', 'span_3.0dB', 'span_6.0dB', 'q_factor'):
|
||||
for label in (
|
||||
"octave_l",
|
||||
"octave_r",
|
||||
"decade_l",
|
||||
"decade_r",
|
||||
"freq_center",
|
||||
"span_3.0dB",
|
||||
"span_6.0dB",
|
||||
"q_factor",
|
||||
):
|
||||
self.label[label] = QtWidgets.QLabel()
|
||||
for attn in CUTOFF_VALS:
|
||||
self.label[f"{attn:.1f}dB_l"] = QtWidgets.QLabel()
|
||||
self.label[f"{attn:.1f}dB_r"] = QtWidgets.QLabel()
|
||||
|
||||
layout = self.layout
|
||||
layout.addRow(self.label['titel'])
|
||||
layout.addRow(self.label["titel"])
|
||||
layout.addRow(
|
||||
QtWidgets.QLabel(
|
||||
f"Please place {self.app.markers[0].name}"
|
||||
f" in the filter passband."))
|
||||
layout.addRow("Result:", self.label['result'])
|
||||
f" in the filter passband."
|
||||
)
|
||||
)
|
||||
layout.addRow("Result:", self.label["result"])
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
layout.addRow("Center frequency:", self.label['freq_center'])
|
||||
layout.addRow("Bandwidth (-3 dB):", self.label['span_3.0dB'])
|
||||
layout.addRow("Quality factor:", self.label['q_factor'])
|
||||
layout.addRow("Bandwidth (-6 dB):", self.label['span_6.0dB'])
|
||||
layout.addRow("Center frequency:", self.label["freq_center"])
|
||||
layout.addRow("Bandwidth (-3 dB):", self.label["span_3.0dB"])
|
||||
layout.addRow("Quality factor:", self.label["q_factor"])
|
||||
layout.addRow("Bandwidth (-6 dB):", self.label["span_6.0dB"])
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
layout.addRow(QtWidgets.QLabel("Lower side:"))
|
||||
layout.addRow("Cutoff frequency:", self.label['3.0dB_l'])
|
||||
layout.addRow("-6 dB point:", self.label['6.0dB_l'])
|
||||
layout.addRow("-60 dB point:", self.label['60.0dB_l'])
|
||||
layout.addRow("Roll-off:", self.label['octave_l'])
|
||||
layout.addRow("Roll-off:", self.label['decade_l'])
|
||||
layout.addRow("Cutoff frequency:", self.label["3.0dB_l"])
|
||||
layout.addRow("-6 dB point:", self.label["6.0dB_l"])
|
||||
layout.addRow("-60 dB point:", self.label["60.0dB_l"])
|
||||
layout.addRow("Roll-off:", self.label["octave_l"])
|
||||
layout.addRow("Roll-off:", self.label["decade_l"])
|
||||
layout.addRow(QtWidgets.QLabel(""))
|
||||
|
||||
layout.addRow(QtWidgets.QLabel("Upper side:"))
|
||||
layout.addRow("Cutoff frequency:", self.label['3.0dB_r'])
|
||||
layout.addRow("-6 dB point:", self.label['6.0dB_r'])
|
||||
layout.addRow("-60 dB point:", self.label['60.0dB_r'])
|
||||
layout.addRow("Roll-off:", self.label['octave_r'])
|
||||
layout.addRow("Roll-off:", self.label['decade_r'])
|
||||
layout.addRow("Cutoff frequency:", self.label["3.0dB_r"])
|
||||
layout.addRow("-6 dB point:", self.label["6.0dB_r"])
|
||||
layout.addRow("-60 dB point:", self.label["60.0dB_r"])
|
||||
layout.addRow("Roll-off:", self.label["octave_r"])
|
||||
layout.addRow("Roll-off:", self.label["decade_r"])
|
||||
|
||||
self.set_titel("Band pass filter analysis")
|
||||
|
||||
|
@ -103,72 +113,90 @@ class BandPassAnalysis(Analysis):
|
|||
self.derive_60dB(cutoff_pos, cutoff_freq)
|
||||
|
||||
result = {
|
||||
'span_3.0dB': cutoff_freq['3.0dB_r'] - cutoff_freq['3.0dB_l'],
|
||||
'span_6.0dB': cutoff_freq['6.0dB_r'] - cutoff_freq['6.0dB_l'],
|
||||
'freq_center':
|
||||
math.sqrt(cutoff_freq['3.0dB_l'] * cutoff_freq['3.0dB_r']),
|
||||
"span_3.0dB": cutoff_freq["3.0dB_r"] - cutoff_freq["3.0dB_l"],
|
||||
"span_6.0dB": cutoff_freq["6.0dB_r"] - cutoff_freq["6.0dB_l"],
|
||||
"freq_center": math.sqrt(
|
||||
cutoff_freq["3.0dB_l"] * cutoff_freq["3.0dB_r"]
|
||||
),
|
||||
}
|
||||
result['q_factor'] = result['freq_center'] / result['span_3.0dB']
|
||||
result["q_factor"] = result["freq_center"] / result["span_3.0dB"]
|
||||
|
||||
result['octave_l'], result['decade_l'] = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"])
|
||||
result['octave_r'], result['decade_r'] = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"])
|
||||
result["octave_l"], result["decade_l"] = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB_l"], cutoff_pos["20.0dB_l"]
|
||||
)
|
||||
result["octave_r"], result["decade_r"] = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB_r"], cutoff_pos["20.0dB_r"]
|
||||
)
|
||||
|
||||
for label, val in cutoff_freq.items():
|
||||
self.label[label].setText(
|
||||
f"{format_frequency(val)}"
|
||||
f" ({cutoff_gain[label]:.1f} dB)")
|
||||
for label in ('freq_center', 'span_3.0dB', 'span_6.0dB'):
|
||||
f"{format_frequency(val)}" f" ({cutoff_gain[label]:.1f} dB)"
|
||||
)
|
||||
for label in ("freq_center", "span_3.0dB", "span_6.0dB"):
|
||||
self.label[label].setText(format_frequency(result[label]))
|
||||
self.label['q_factor'].setText(f"{result['q_factor']:.2f}")
|
||||
self.label["q_factor"].setText(f"{result['q_factor']:.2f}")
|
||||
|
||||
for label in ('octave_l', 'decade_l', 'octave_r', 'decade_r'):
|
||||
for label in ("octave_l", "decade_l", "octave_r", "decade_r"):
|
||||
self.label[label].setText(f"{result[label]:.3f}dB/{label[:-2]}")
|
||||
|
||||
self.app.markers[0].setFrequency(f"{result['freq_center']}")
|
||||
self.app.markers[1].setFrequency(f"{cutoff_freq['3.0dB_l']}")
|
||||
self.app.markers[2].setFrequency(f"{cutoff_freq['3.0dB_r']}")
|
||||
|
||||
if cutoff_gain['3.0dB_l'] < -4 or cutoff_gain['3.0dB_r'] < -4:
|
||||
if cutoff_gain["3.0dB_l"] < -4 or cutoff_gain["3.0dB_r"] < -4:
|
||||
logger.warning(
|
||||
"Data points insufficient for true -3 dB points."
|
||||
"Cutoff gains: %fdB, %fdB", cutoff_gain['3.0dB_l'],
|
||||
cutoff_gain['3.0dB_r'])
|
||||
"Cutoff gains: %fdB, %fdB",
|
||||
cutoff_gain["3.0dB_l"],
|
||||
cutoff_gain["3.0dB_r"],
|
||||
)
|
||||
self.set_result(
|
||||
f"Analysis complete ({len(s21)} points)\n"
|
||||
f"Insufficient data for analysis. Increase segment count.")
|
||||
f"Insufficient data for analysis. Increase segment count."
|
||||
)
|
||||
return
|
||||
self.set_result(f"Analysis complete ({len(s21)} points)")
|
||||
|
||||
def derive_60dB(self,
|
||||
cutoff_pos: Dict[str, int],
|
||||
cutoff_freq: Dict[str, float]):
|
||||
def derive_60dB(
|
||||
self, cutoff_pos: Dict[str, int], cutoff_freq: Dict[str, float]
|
||||
):
|
||||
"""derive 60dB cutoff if needed an possible
|
||||
|
||||
Args:
|
||||
cutoff_pos (Dict[str, int])
|
||||
cutoff_freq (Dict[str, float])
|
||||
"""
|
||||
if (math.isnan(cutoff_freq['60.0dB_l']) and
|
||||
cutoff_pos['20.0dB_l'] != -1 and cutoff_pos['10.0dB_l'] != -1):
|
||||
cutoff_freq['60.0dB_l'] = (
|
||||
cutoff_freq["10.0dB_l"] *
|
||||
10 ** (5 * (math.log10(cutoff_pos['20.0dB_l']) -
|
||||
math.log10(cutoff_pos['10.0dB_l']))))
|
||||
if (math.isnan(cutoff_freq['60.0dB_r']) and
|
||||
cutoff_pos['20.0dB_r'] != -1 and cutoff_pos['10.0dB_r'] != -1):
|
||||
cutoff_freq['60.0dB_r'] = (
|
||||
cutoff_freq["10.0dB_r"] *
|
||||
10 ** (5 * (math.log10(cutoff_pos['20.0dB_r']) -
|
||||
math.log10(cutoff_pos['10.0dB_r'])
|
||||
)))
|
||||
if (
|
||||
math.isnan(cutoff_freq["60.0dB_l"])
|
||||
and cutoff_pos["20.0dB_l"] != -1
|
||||
and cutoff_pos["10.0dB_l"] != -1
|
||||
):
|
||||
cutoff_freq["60.0dB_l"] = cutoff_freq["10.0dB_l"] * 10 ** (
|
||||
5
|
||||
* (
|
||||
math.log10(cutoff_pos["20.0dB_l"])
|
||||
- math.log10(cutoff_pos["10.0dB_l"])
|
||||
)
|
||||
)
|
||||
if (
|
||||
math.isnan(cutoff_freq["60.0dB_r"])
|
||||
and cutoff_pos["20.0dB_r"] != -1
|
||||
and cutoff_pos["10.0dB_r"] != -1
|
||||
):
|
||||
cutoff_freq["60.0dB_r"] = cutoff_freq["10.0dB_r"] * 10 ** (
|
||||
5
|
||||
* (
|
||||
math.log10(cutoff_pos["20.0dB_r"])
|
||||
- math.log10(cutoff_pos["10.0dB_r"])
|
||||
)
|
||||
)
|
||||
|
||||
def find_center(self, gains: List[float]) -> int:
|
||||
marker = self.app.markers[0]
|
||||
if marker.location <= 0 or marker.location >= len(gains) - 1:
|
||||
logger.debug("No valid location for %s (%s)",
|
||||
marker.name, marker.location)
|
||||
logger.debug(
|
||||
"No valid location for %s (%s)", marker.name, marker.location
|
||||
)
|
||||
self.set_result(f"Please place {marker.name} in the passband.")
|
||||
return -1
|
||||
|
||||
|
@ -178,13 +206,15 @@ class BandPassAnalysis(Analysis):
|
|||
return -1
|
||||
return peak
|
||||
|
||||
def find_bounderies(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
def find_bounderies(
|
||||
self, gains: List[float], peak: int, peak_db: float
|
||||
) -> Dict[str, int]:
|
||||
cutoff_pos = {}
|
||||
for attn in CUTOFF_VALS:
|
||||
cutoff_pos[f"{attn:.1f}dB_l"] = at.cut_off_left(
|
||||
gains, peak, peak_db, attn)
|
||||
gains, peak, peak_db, attn
|
||||
)
|
||||
cutoff_pos[f"{attn:.1f}dB_r"] = at.cut_off_right(
|
||||
gains, peak, peak_db, attn)
|
||||
gains, peak, peak_db, attn
|
||||
)
|
||||
return cutoff_pos
|
||||
|
|
|
@ -34,11 +34,13 @@ class BandStopAnalysis(BandPassAnalysis):
|
|||
def find_center(self, gains: List[float]) -> int:
|
||||
return max(enumerate(gains), key=lambda i: i[1])[0]
|
||||
|
||||
def find_bounderies(self,
|
||||
gains: List[float],
|
||||
_: int, peak_db: float) -> Dict[str, int]:
|
||||
def find_bounderies(
|
||||
self, gains: List[float], _: int, peak_db: float
|
||||
) -> Dict[str, int]:
|
||||
cutoff_pos = {}
|
||||
for attn in CUTOFF_VALS:
|
||||
cutoff_pos[f"{attn:.1f}dB_l"], cutoff_pos[f"{attn:.1f}dB_r"] = (
|
||||
at.dip_cut_offs(gains, peak_db, attn))
|
||||
(
|
||||
cutoff_pos[f"{attn:.1f}dB_l"],
|
||||
cutoff_pos[f"{attn:.1f}dB_r"],
|
||||
) = at.dip_cut_offs(gains, peak_db, attn)
|
||||
return cutoff_pos
|
||||
|
|
|
@ -35,8 +35,8 @@ class Analysis:
|
|||
def __init__(self, app: QtWidgets.QWidget):
|
||||
self.app = app
|
||||
self.label: Dict[str, QtWidgets.QLabel] = {
|
||||
'titel': QtWidgets.QLabel(),
|
||||
'result': QtWidgets.QLabel(),
|
||||
"titel": QtWidgets.QLabel(),
|
||||
"result": QtWidgets.QLabel(),
|
||||
}
|
||||
self.layout = QtWidgets.QFormLayout()
|
||||
self._widget = QtWidgets.QWidget()
|
||||
|
@ -53,7 +53,7 @@ class Analysis:
|
|||
label.clear()
|
||||
|
||||
def set_result(self, text):
|
||||
self.label['result'].setText(text)
|
||||
self.label["result"].setText(text)
|
||||
|
||||
def set_titel(self, text):
|
||||
self.label['titel'].setText(text)
|
||||
self.label["titel"].setText(text)
|
||||
|
|
|
@ -23,10 +23,14 @@ from PyQt5 import QtWidgets
|
|||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.ResonanceAnalysis import (
|
||||
ResonanceAnalysis, format_resistence_neg
|
||||
ResonanceAnalysis,
|
||||
format_resistence_neg,
|
||||
)
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency, format_complex_imp, format_frequency_short)
|
||||
format_frequency,
|
||||
format_complex_imp,
|
||||
format_frequency_short,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -43,8 +47,8 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
def do_resonance_analysis(self):
|
||||
s11 = self.app.data.s11
|
||||
maximums = sorted(
|
||||
at.maxima([d.impedance().real for d in s11],
|
||||
threshold=500))
|
||||
at.maxima([d.impedance().real for d in s11], threshold=500)
|
||||
)
|
||||
extended_data = {}
|
||||
logger.info("TO DO: find near data")
|
||||
for lowest in self.crossings:
|
||||
|
@ -61,12 +65,14 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
extended_data[m].update(my_data)
|
||||
else:
|
||||
extended_data[m] = my_data
|
||||
fields = [("freq", format_frequency_short),
|
||||
("r", format_resistence_neg), ("lambda", lambda x: round(x, 2))]
|
||||
fields = [
|
||||
("freq", format_frequency_short),
|
||||
("r", format_resistence_neg),
|
||||
("lambda", lambda x: round(x, 2)),
|
||||
]
|
||||
|
||||
if self.old_data:
|
||||
diff = self.compare(
|
||||
self.old_data[-1], extended_data, fields=fields)
|
||||
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)
|
||||
|
@ -76,14 +82,17 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
QtWidgets.QLabel(
|
||||
f" ({diff[i]['freq']})"
|
||||
f" {format_complex_imp(s11[idx].impedance())}"
|
||||
f" ({diff[i]['r']}) {diff[i]['lambda']} m"))
|
||||
f" ({diff[i]['r']}) {diff[i]['lambda']} m"
|
||||
),
|
||||
)
|
||||
|
||||
if self.filename and extended_data:
|
||||
with open(
|
||||
self.filename, 'w', newline='', encoding='utf-8'
|
||||
self.filename, "w", newline="", encoding="utf-8"
|
||||
) as csvfile:
|
||||
fieldnames = extended_data[sorted(
|
||||
extended_data.keys())[0]].keys()
|
||||
fieldnames = extended_data[
|
||||
sorted(extended_data.keys())[0]
|
||||
].keys()
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
for idx in sorted(extended_data.keys()):
|
||||
|
@ -99,10 +108,11 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
:param old:
|
||||
:param new:
|
||||
"""
|
||||
fields = fields or [("freq", str), ]
|
||||
fields = fields or [
|
||||
("freq", str),
|
||||
]
|
||||
|
||||
def no_compare():
|
||||
|
||||
return {k: "-" for k, _ in fields}
|
||||
|
||||
old_idx = sorted(old.keys())
|
||||
|
@ -113,8 +123,9 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
i_tot = max(len(old_idx), len(new_idx))
|
||||
|
||||
if i_max != i_tot:
|
||||
logger.warning("resonances changed from %s to %s",
|
||||
len(old_idx), len(new_idx))
|
||||
logger.warning(
|
||||
"resonances changed from %s to %s", len(old_idx), len(new_idx)
|
||||
)
|
||||
|
||||
split = 0
|
||||
max_delta_f = 1_000_000
|
||||
|
@ -135,15 +146,19 @@ class EFHWAnalysis(ResonanceAnalysis):
|
|||
logger.debug("Deltas %s", diff[i])
|
||||
continue
|
||||
|
||||
logger.debug("can't compare, %s is too much ",
|
||||
format_frequency(delta_f))
|
||||
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):
|
||||
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 ?!?
|
||||
|
|
|
@ -41,9 +41,12 @@ class HighPassAnalysis(Analysis):
|
|||
|
||||
layout = self.layout
|
||||
layout.addRow(self.label["titel"])
|
||||
layout.addRow(QtWidgets.QLabel(
|
||||
f"Please place {self.app.markers[0].name}"
|
||||
f" in the filter passband."))
|
||||
layout.addRow(
|
||||
QtWidgets.QLabel(
|
||||
f"Please place {self.app.markers[0].name}"
|
||||
f" in the filter passband."
|
||||
)
|
||||
)
|
||||
layout.addRow("Result:", self.label["result"])
|
||||
layout.addRow("Cutoff frequency:", self.label["3.0dB"])
|
||||
layout.addRow("-6 dB point:", self.label["6.0dB"])
|
||||
|
@ -51,7 +54,7 @@ class HighPassAnalysis(Analysis):
|
|||
layout.addRow("Roll-off:", self.label["octave"])
|
||||
layout.addRow("Roll-off:", self.label["decade"])
|
||||
|
||||
self.set_titel('Highpass analysis')
|
||||
self.set_titel("Highpass analysis")
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s21:
|
||||
|
@ -81,25 +84,28 @@ class HighPassAnalysis(Analysis):
|
|||
logger.debug("Cuttoff gains: %s", cutoff_gain)
|
||||
|
||||
octave, decade = at.calculate_rolloff(
|
||||
s21, cutoff_pos["10.0dB"], cutoff_pos["20.0dB"])
|
||||
s21, cutoff_pos["10.0dB"], cutoff_pos["20.0dB"]
|
||||
)
|
||||
|
||||
if cutoff_gain['3.0dB'] < -4:
|
||||
logger.debug("Cutoff frequency found at %f dB"
|
||||
" - insufficient data points for true -3 dB point.",
|
||||
cutoff_gain)
|
||||
logger.debug("Found true cutoff frequency at %d", cutoff_freq['3.0dB'])
|
||||
if cutoff_gain["3.0dB"] < -4:
|
||||
logger.debug(
|
||||
"Cutoff frequency found at %f dB"
|
||||
" - insufficient data points for true -3 dB point.",
|
||||
cutoff_gain,
|
||||
)
|
||||
logger.debug("Found true cutoff frequency at %d", cutoff_freq["3.0dB"])
|
||||
|
||||
for label, val in cutoff_freq.items():
|
||||
self.label[label].setText(
|
||||
f"{format_frequency(val)}"
|
||||
f" ({cutoff_gain[label]:.1f} dB)")
|
||||
f"{format_frequency(val)}" f" ({cutoff_gain[label]:.1f} dB)"
|
||||
)
|
||||
|
||||
self.label['octave'].setText(f'{octave:.3f}dB/octave')
|
||||
self.label['decade'].setText(f'{decade:.3f}dB/decade')
|
||||
self.label["octave"].setText(f"{octave:.3f}dB/octave")
|
||||
self.label["decade"].setText(f"{decade:.3f}dB/decade")
|
||||
|
||||
self.app.markers[0].setFrequency(str(s21[peak].freq))
|
||||
self.app.markers[1].setFrequency(str(cutoff_freq['3.0dB']))
|
||||
self.app.markers[2].setFrequency(str(cutoff_freq['6.0dB']))
|
||||
self.app.markers[1].setFrequency(str(cutoff_freq["3.0dB"]))
|
||||
self.app.markers[2].setFrequency(str(cutoff_freq["6.0dB"]))
|
||||
|
||||
self.set_result(f"Analysis complete ({len(s21)}) points)")
|
||||
|
||||
|
@ -111,11 +117,10 @@ class HighPassAnalysis(Analysis):
|
|||
return -1
|
||||
return at.center_from_idx(gains, marker.location)
|
||||
|
||||
def find_cutoffs(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
def find_cutoffs(
|
||||
self, gains: List[float], peak: int, peak_db: float
|
||||
) -> Dict[str, int]:
|
||||
return {
|
||||
f"{attn:.1f}dB": at.cut_off_left(
|
||||
gains, peak, peak_db, attn)
|
||||
f"{attn:.1f}dB": at.cut_off_left(gains, peak, peak_db, attn)
|
||||
for attn in CUTOFF_VALS
|
||||
}
|
||||
|
|
|
@ -30,13 +30,12 @@ class LowPassAnalysis(HighPassAnalysis):
|
|||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self.set_titel('Lowpass filter analysis')
|
||||
self.set_titel("Lowpass filter analysis")
|
||||
|
||||
def find_cutoffs(self,
|
||||
gains: List[float],
|
||||
peak: int, peak_db: float) -> Dict[str, int]:
|
||||
def find_cutoffs(
|
||||
self, gains: List[float], peak: int, peak_db: float
|
||||
) -> Dict[str, int]:
|
||||
return {
|
||||
f"{attn:.1f}dB": at.cut_off_right(
|
||||
gains, peak, peak_db, attn)
|
||||
f"{attn:.1f}dB": at.cut_off_right(gains, peak, peak_db, attn)
|
||||
for attn in CUTOFF_VALS
|
||||
}
|
||||
|
|
|
@ -20,12 +20,14 @@ import logging
|
|||
|
||||
from PyQt5 import QtWidgets
|
||||
import numpy as np
|
||||
|
||||
# pylint: disable=import-error, no-name-in-module
|
||||
from scipy.signal import find_peaks, peak_prominences
|
||||
|
||||
from NanoVNASaver.Analysis.Base import QHLine
|
||||
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import (
|
||||
SimplePeakSearchAnalysis)
|
||||
SimplePeakSearchAnalysis,
|
||||
)
|
||||
|
||||
from NanoVNASaver.Formatting import format_frequency_short
|
||||
|
||||
|
@ -34,7 +36,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
|
@ -48,7 +49,7 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
|||
self.layout.addRow(QtWidgets.QLabel("<b>Results</b>"))
|
||||
self.results_header = self.layout.rowCount()
|
||||
|
||||
self.set_titel('Peak search')
|
||||
self.set_titel("Peak search")
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s11:
|
||||
|
@ -59,14 +60,14 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
|||
data, fmt_fnc = self.data_and_format()
|
||||
|
||||
inverted = False
|
||||
if self.button['peak_l'].isChecked():
|
||||
if self.button["peak_l"].isChecked():
|
||||
inverted = True
|
||||
peaks, _ = find_peaks(
|
||||
-np.array(data), width=3, distance=3, prominence=1)
|
||||
-np.array(data), width=3, distance=3, prominence=1
|
||||
)
|
||||
else:
|
||||
self.button['peak_h'].setChecked(True)
|
||||
peaks, _ = find_peaks(
|
||||
data, width=3, distance=3, prominence=1)
|
||||
self.button["peak_h"].setChecked(True)
|
||||
peaks, _ = find_peaks(data, width=3, distance=3, prominence=1)
|
||||
|
||||
# Having found the peaks, get the prominence data
|
||||
for i, p in np.ndenumerate(peaks):
|
||||
|
@ -89,19 +90,24 @@ class PeakSearchAnalysis(SimplePeakSearchAnalysis):
|
|||
f"Freq: {format_frequency_short(s11[pos].freq)}",
|
||||
QtWidgets.QLabel(
|
||||
f" Value: {fmt_fnc(-data[pos] if inverted else data[pos])}"
|
||||
))
|
||||
),
|
||||
)
|
||||
|
||||
if self.button['move_marker'].isChecked():
|
||||
if self.button["move_marker"].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))):
|
||||
self.app.markers[i].setFrequency(
|
||||
str(s11[peaks[indices[i]]].freq))
|
||||
str(s11[peaks[indices[i]]].freq)
|
||||
)
|
||||
|
||||
def reset(self):
|
||||
super().reset()
|
||||
logger.debug("Results start at %d, out of %d",
|
||||
self.results_header, self.layout.rowCount())
|
||||
logger.debug(
|
||||
"Results start at %d, out of %d",
|
||||
self.results_header,
|
||||
self.layout.rowCount(),
|
||||
)
|
||||
for _ in range(self.results_header, self.layout.rowCount()):
|
||||
logger.debug("deleting %s", self.layout.rowCount())
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
|
|
|
@ -25,9 +25,7 @@ from PyQt5 import QtWidgets
|
|||
|
||||
import NanoVNASaver.AnalyticTools as at
|
||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency, format_complex_imp,
|
||||
format_resistance)
|
||||
from NanoVNASaver.Formatting import format_frequency, format_resistance
|
||||
from NanoVNASaver.RFTools import reflection_coefficient
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
@ -44,7 +42,6 @@ def vswr_transformed(z, ratio=49) -> float:
|
|||
|
||||
|
||||
class ResonanceAnalysis(Analysis):
|
||||
|
||||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
self.crossings: List[int] = []
|
||||
|
@ -72,10 +69,8 @@ class ResonanceAnalysis(Analysis):
|
|||
"impedance": s11[index].impedance(),
|
||||
"vswr": s11[index].vswr,
|
||||
}
|
||||
my_data["vswr_49"] = vswr_transformed(
|
||||
my_data["impedance"], 49)
|
||||
my_data["vswr_4"] = vswr_transformed(
|
||||
my_data["impedance"], 4)
|
||||
my_data["vswr_49"] = vswr_transformed(my_data["impedance"], 49)
|
||||
my_data["vswr_4"] = vswr_transformed(my_data["impedance"], 4)
|
||||
my_data["r"] = my_data["impedance"].real
|
||||
my_data["x"] = my_data["impedance"].imag
|
||||
|
||||
|
@ -83,24 +78,28 @@ class ResonanceAnalysis(Analysis):
|
|||
|
||||
def runAnalysis(self):
|
||||
self.reset()
|
||||
self.filename = os.path.join(
|
||||
"/tmp/", f"{self.input_description.text()}.csv"
|
||||
) if self.input_description.text() else ""
|
||||
self.filename = (
|
||||
os.path.join("/tmp/", f"{self.input_description.text()}.csv")
|
||||
if self.input_description.text()
|
||||
else ""
|
||||
)
|
||||
|
||||
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 _ in range(results_header, self.layout.rowCount()):
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
|
||||
self.crossings = sorted(
|
||||
set(at.zero_crossings([d.phase for d in self.app.data.s11])))
|
||||
logger.debug("Found %d sections ",
|
||||
len(self.crossings))
|
||||
set(at.zero_crossings([d.phase for d in self.app.data.s11]))
|
||||
)
|
||||
logger.debug("Found %d sections ", len(self.crossings))
|
||||
if not self.crossings:
|
||||
self.layout.addRow(QtWidgets.QLabel(
|
||||
"No resonance found"))
|
||||
self.layout.addRow(QtWidgets.QLabel("No resonance found"))
|
||||
return
|
||||
|
||||
self
|
||||
|
@ -111,14 +110,18 @@ class ResonanceAnalysis(Analysis):
|
|||
extended_data = []
|
||||
for crossing in self.crossings:
|
||||
extended_data.append(self._get_data(crossing))
|
||||
self.layout.addRow("Resonance", QtWidgets.QLabel(
|
||||
format_frequency(self.app.data.s11[crossing].freq)))
|
||||
self.layout.addRow(
|
||||
"Resonance",
|
||||
QtWidgets.QLabel(
|
||||
format_frequency(self.app.data.s11[crossing].freq)
|
||||
),
|
||||
)
|
||||
self.layout.addWidget(QHLine())
|
||||
# Remove the final separator line
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
if self.filename and extended_data:
|
||||
with open(
|
||||
self.filename, 'w', encoding='utf-8', newline=''
|
||||
self.filename, "w", encoding="utf-8", newline=""
|
||||
) as csvfile:
|
||||
fieldnames = extended_data[0].keys()
|
||||
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
||||
|
|
|
@ -24,7 +24,11 @@ import numpy as np
|
|||
|
||||
from NanoVNASaver.Analysis.Base import Analysis, QHLine
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency, format_gain, format_resistance, format_vswr)
|
||||
format_frequency,
|
||||
format_gain,
|
||||
format_resistance,
|
||||
format_vswr,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -33,51 +37,51 @@ class SimplePeakSearchAnalysis(Analysis):
|
|||
def __init__(self, app):
|
||||
super().__init__(app)
|
||||
|
||||
self.label['peak_freq'] = QtWidgets.QLabel()
|
||||
self.label['peak_db'] = QtWidgets.QLabel()
|
||||
self.label["peak_freq"] = QtWidgets.QLabel()
|
||||
self.label["peak_db"] = QtWidgets.QLabel()
|
||||
|
||||
self.button = {
|
||||
'vswr': QtWidgets.QRadioButton("VSWR"),
|
||||
'resistance': QtWidgets.QRadioButton("Resistance"),
|
||||
'reactance': QtWidgets.QRadioButton("Reactance"),
|
||||
'gain': QtWidgets.QRadioButton("S21 Gain"),
|
||||
'peak_h': QtWidgets.QRadioButton("Highest value"),
|
||||
'peak_l': QtWidgets.QRadioButton("Lowest value"),
|
||||
'move_marker': QtWidgets.QCheckBox()
|
||||
"vswr": QtWidgets.QRadioButton("VSWR"),
|
||||
"resistance": QtWidgets.QRadioButton("Resistance"),
|
||||
"reactance": QtWidgets.QRadioButton("Reactance"),
|
||||
"gain": QtWidgets.QRadioButton("S21 Gain"),
|
||||
"peak_h": QtWidgets.QRadioButton("Highest value"),
|
||||
"peak_l": QtWidgets.QRadioButton("Lowest value"),
|
||||
"move_marker": QtWidgets.QCheckBox(),
|
||||
}
|
||||
|
||||
self.button['gain'].setChecked(True)
|
||||
self.button['peak_h'].setChecked(True)
|
||||
self.button["gain"].setChecked(True)
|
||||
self.button["peak_h"].setChecked(True)
|
||||
|
||||
self.btn_group = {
|
||||
'data': QtWidgets.QButtonGroup(),
|
||||
'peak': QtWidgets.QButtonGroup(),
|
||||
"data": QtWidgets.QButtonGroup(),
|
||||
"peak": QtWidgets.QButtonGroup(),
|
||||
}
|
||||
|
||||
for btn in ('vswr', 'resistance', 'reactance', 'gain'):
|
||||
self.btn_group['data'].addButton(self.button[btn])
|
||||
self.btn_group['peak'].addButton(self.button['peak_h'])
|
||||
self.btn_group['peak'].addButton(self.button['peak_l'])
|
||||
for btn in ("vswr", "resistance", "reactance", "gain"):
|
||||
self.btn_group["data"].addButton(self.button[btn])
|
||||
self.btn_group["peak"].addButton(self.button["peak_h"])
|
||||
self.btn_group["peak"].addButton(self.button["peak_l"])
|
||||
|
||||
layout = self.layout
|
||||
layout.addRow(self.label['titel'])
|
||||
layout.addRow(self.label["titel"])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow(QtWidgets.QLabel("<b>Settings</b>"))
|
||||
layout.addRow("Data source", self.button['vswr'])
|
||||
layout.addRow("", self.button['resistance'])
|
||||
layout.addRow("", self.button['reactance'])
|
||||
layout.addRow("", self.button['gain'])
|
||||
layout.addRow("Data source", self.button["vswr"])
|
||||
layout.addRow("", self.button["resistance"])
|
||||
layout.addRow("", self.button["reactance"])
|
||||
layout.addRow("", self.button["gain"])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow("Peak type", self.button['peak_h'])
|
||||
layout.addRow("", self.button['peak_l'])
|
||||
layout.addRow("Peak type", self.button["peak_h"])
|
||||
layout.addRow("", self.button["peak_l"])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow("Move marker to peak", self.button['move_marker'])
|
||||
layout.addRow("Move marker to peak", self.button["move_marker"])
|
||||
layout.addRow(QHLine())
|
||||
layout.addRow(self.label['result'])
|
||||
layout.addRow("Peak frequency:", self.label['peak_freq'])
|
||||
layout.addRow("Peak value:", self.label['peak_db'])
|
||||
layout.addRow(self.label["result"])
|
||||
layout.addRow("Peak frequency:", self.label["peak_freq"])
|
||||
layout.addRow("Peak value:", self.label["peak_db"])
|
||||
|
||||
self.set_titel('Simple peak search')
|
||||
self.set_titel("Simple peak search")
|
||||
|
||||
def runAnalysis(self):
|
||||
if not self.app.data.s11:
|
||||
|
@ -86,16 +90,16 @@ class SimplePeakSearchAnalysis(Analysis):
|
|||
s11 = self.app.data.s11
|
||||
data, fmt_fnc = self.data_and_format()
|
||||
|
||||
if self.button['peak_l'].isChecked():
|
||||
if self.button["peak_l"].isChecked():
|
||||
idx_peak = np.argmin(data)
|
||||
else:
|
||||
self.button['peak_h'].setChecked(True)
|
||||
self.button["peak_h"].setChecked(True)
|
||||
idx_peak = np.argmax(data)
|
||||
|
||||
self.label['peak_freq'].setText(format_frequency(s11[idx_peak].freq))
|
||||
self.label['peak_db'].setText(fmt_fnc(data[idx_peak]))
|
||||
self.label["peak_freq"].setText(format_frequency(s11[idx_peak].freq))
|
||||
self.label["peak_db"].setText(fmt_fnc(data[idx_peak]))
|
||||
|
||||
if self.button['move_marker'].isChecked() and self.app.markers:
|
||||
if self.button["move_marker"].isChecked() and self.app.markers:
|
||||
self.app.markers[0].setFrequency(f"{s11[idx_peak].freq}")
|
||||
|
||||
def data_and_format(self) -> Tuple[List[float], Callable]:
|
||||
|
@ -103,17 +107,17 @@ class SimplePeakSearchAnalysis(Analysis):
|
|||
s21 = self.app.data.s21
|
||||
|
||||
if not s21:
|
||||
self.button['gain'].setEnabled(False)
|
||||
if self.button['gain'].isChecked():
|
||||
self.button['vswr'].setChecked(True)
|
||||
self.button["gain"].setEnabled(False)
|
||||
if self.button["gain"].isChecked():
|
||||
self.button["vswr"].setChecked(True)
|
||||
else:
|
||||
self.button['gain'].setEnabled(True)
|
||||
self.button["gain"].setEnabled(True)
|
||||
|
||||
if self.button['gain'].isChecked():
|
||||
if self.button["gain"].isChecked():
|
||||
return ([d.gain for d in s21], format_gain)
|
||||
if self.button['resistance'].isChecked():
|
||||
if self.button["resistance"].isChecked():
|
||||
return ([d.impedance().real for d in s11], format_resistance)
|
||||
if self.button['reactance'].isChecked():
|
||||
if self.button["reactance"].isChecked():
|
||||
return ([d.impedance().imag for d in s11], format_resistance)
|
||||
# default
|
||||
return ([d.vswr for d in s11], format_vswr)
|
||||
|
|
|
@ -64,34 +64,50 @@ class VSWRAnalysis(Analysis):
|
|||
data = [d.vswr for d in s11]
|
||||
threshold = self.input_vswr_limit.value()
|
||||
|
||||
minima = sorted(at.minima(data, threshold),
|
||||
key=lambda i: data[i])[:VSWRAnalysis.max_dips_shown]
|
||||
minima = sorted(at.minima(data, threshold), key=lambda i: data[i])[
|
||||
: VSWRAnalysis.max_dips_shown
|
||||
]
|
||||
self.minimums = minima
|
||||
|
||||
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 _ in range(results_header, self.layout.rowCount()):
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
|
||||
if not minima:
|
||||
self.layout.addRow(QtWidgets.QLabel(
|
||||
f"No areas found with VSWR below {format_vswr(threshold)}."))
|
||||
self.layout.addRow(
|
||||
QtWidgets.QLabel(
|
||||
f"No areas found with VSWR below {format_vswr(threshold)}."
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
for idx in minima:
|
||||
rng = at.take_from_idx(data, idx, lambda i: i[1] < threshold)
|
||||
begin, end = rng[0], rng[-1]
|
||||
self.layout.addRow("Start", QtWidgets.QLabel(
|
||||
format_frequency(s11[begin].freq)))
|
||||
self.layout.addRow("Minimum", QtWidgets.QLabel(
|
||||
f"{format_frequency(s11[idx].freq)}"
|
||||
f" ({round(s11[idx].vswr, 2)})"))
|
||||
self.layout.addRow("End", QtWidgets.QLabel(
|
||||
format_frequency(s11[end].freq)))
|
||||
self.layout.addRow(
|
||||
"Span", QtWidgets.QLabel(format_frequency(
|
||||
(s11[end].freq - s11[begin].freq))))
|
||||
"Start", QtWidgets.QLabel(format_frequency(s11[begin].freq))
|
||||
)
|
||||
self.layout.addRow(
|
||||
"Minimum",
|
||||
QtWidgets.QLabel(
|
||||
f"{format_frequency(s11[idx].freq)}"
|
||||
f" ({round(s11[idx].vswr, 2)})"
|
||||
),
|
||||
)
|
||||
self.layout.addRow(
|
||||
"End", QtWidgets.QLabel(format_frequency(s11[end].freq))
|
||||
)
|
||||
self.layout.addRow(
|
||||
"Span",
|
||||
QtWidgets.QLabel(
|
||||
format_frequency((s11[end].freq - s11[begin].freq))
|
||||
),
|
||||
)
|
||||
self.layout.addWidget(QHLine())
|
||||
|
||||
self.layout.removeRow(self.layout.rowCount() - 1)
|
||||
|
|
|
@ -21,6 +21,7 @@ import math
|
|||
from typing import Callable, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
# pylint: disable=import-error, no-name-in-module
|
||||
from scipy.signal import find_peaks
|
||||
|
||||
|
@ -42,8 +43,9 @@ def zero_crossings(data: List[float]) -> List[int]:
|
|||
np_data = np.array(data)
|
||||
|
||||
# start with real zeros (ignore first and last element)
|
||||
real_zeros = [n for n in np.where(np_data == 0.0)[0] if
|
||||
n not in {0, np_data.size - 1}]
|
||||
real_zeros = [
|
||||
n for n in np.where(np_data == 0.0)[0] if n not in {0, np_data.size - 1}
|
||||
]
|
||||
# now multipy elements to find change in signess
|
||||
crossings = [
|
||||
n if abs(np_data[n]) < abs(np_data[n + 1]) else n + 1
|
||||
|
@ -61,11 +63,8 @@ def maxima(data: List[float], threshold: float = 0.0) -> List[int]:
|
|||
Returns:
|
||||
List[int]: indices of maxima
|
||||
"""
|
||||
peaks = find_peaks(
|
||||
data, width=2, distance=3, prominence=1)[0].tolist()
|
||||
return [
|
||||
i for i in peaks if data[i] > threshold
|
||||
] if threshold else peaks
|
||||
peaks = find_peaks(data, width=2, distance=3, prominence=1)[0].tolist()
|
||||
return [i for i in peaks if data[i] > threshold] if threshold else peaks
|
||||
|
||||
|
||||
def minima(data: List[float], threshold: float = 0.0) -> List[int]:
|
||||
|
@ -77,16 +76,15 @@ def minima(data: List[float], threshold: float = 0.0) -> List[int]:
|
|||
Returns:
|
||||
List[int]: indices of minima
|
||||
"""
|
||||
bottoms = find_peaks(
|
||||
-np.array(data), width=2, distance=3, prominence=1)[0].tolist()
|
||||
return [
|
||||
i for i in bottoms if data[i] < threshold
|
||||
] if threshold else bottoms
|
||||
bottoms = find_peaks(-np.array(data), width=2, distance=3, prominence=1)[
|
||||
0
|
||||
].tolist()
|
||||
return [i for i in bottoms if data[i] < threshold] if threshold else bottoms
|
||||
|
||||
|
||||
def take_from_idx(data: List[float],
|
||||
idx: int,
|
||||
predicate: Callable) -> List[int]:
|
||||
def take_from_idx(
|
||||
data: List[float], idx: int, predicate: Callable
|
||||
) -> List[int]:
|
||||
"""take_from_center
|
||||
|
||||
Args:
|
||||
|
@ -99,18 +97,21 @@ def take_from_idx(data: List[float],
|
|||
List[int]: indices of element matching predicate left
|
||||
and right from index
|
||||
"""
|
||||
lower = list(reversed(
|
||||
[i for i, _ in
|
||||
it.takewhile(predicate,
|
||||
reversed(list(enumerate(data[:idx]))))]))
|
||||
upper = [i for i, _ in
|
||||
it.takewhile(predicate,
|
||||
enumerate(data[idx:], idx))]
|
||||
lower = list(
|
||||
reversed(
|
||||
[
|
||||
i
|
||||
for i, _ in it.takewhile(
|
||||
predicate, reversed(list(enumerate(data[:idx])))
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
upper = [i for i, _ in it.takewhile(predicate, enumerate(data[idx:], idx))]
|
||||
return lower + upper
|
||||
|
||||
|
||||
def center_from_idx(gains: List[float],
|
||||
idx: int, delta: float = 3.0) -> int:
|
||||
def center_from_idx(gains: List[float], idx: int, delta: float = 3.0) -> int:
|
||||
"""find maximum from index postion of gains in a attn dB gain span
|
||||
|
||||
Args:
|
||||
|
@ -122,13 +123,13 @@ def center_from_idx(gains: List[float],
|
|||
int: position of highest gain from start in range (-1 if no data)
|
||||
"""
|
||||
peak_db = gains[idx]
|
||||
rng = take_from_idx(gains, idx,
|
||||
lambda i: abs(peak_db - i[1]) < delta)
|
||||
rng = take_from_idx(gains, idx, lambda i: abs(peak_db - i[1]) < delta)
|
||||
return max(rng, key=lambda i: gains[i]) if rng else -1
|
||||
|
||||
|
||||
def cut_off_left(gains: List[float], idx: int,
|
||||
peak_gain: float, attn: float = 3.0) -> int:
|
||||
def cut_off_left(
|
||||
gains: List[float], idx: int, peak_gain: float, attn: float = 3.0
|
||||
) -> int:
|
||||
"""find first position in list where gain in attn lower then peak
|
||||
left from index
|
||||
|
||||
|
@ -143,13 +144,13 @@ def cut_off_left(gains: List[float], idx: int,
|
|||
int: position of attenuation point. (-1 if no data)
|
||||
"""
|
||||
return next(
|
||||
(i for i in range(idx, -1, -1) if
|
||||
(peak_gain - gains[i]) > attn),
|
||||
-1)
|
||||
(i for i in range(idx, -1, -1) if (peak_gain - gains[i]) > attn), -1
|
||||
)
|
||||
|
||||
|
||||
def cut_off_right(gains: List[float], idx: int,
|
||||
peak_gain: float, attn: float = 3.0) -> int:
|
||||
def cut_off_right(
|
||||
gains: List[float], idx: int, peak_gain: float, attn: float = 3.0
|
||||
) -> int:
|
||||
"""find first position in list where gain in attn lower then peak
|
||||
right from index
|
||||
|
||||
|
@ -165,19 +166,20 @@ def cut_off_right(gains: List[float], idx: int,
|
|||
"""
|
||||
|
||||
return next(
|
||||
(i for i in range(idx, len(gains)) if
|
||||
(peak_gain - gains[i]) > attn),
|
||||
-1)
|
||||
(i for i in range(idx, len(gains)) if (peak_gain - gains[i]) > attn), -1
|
||||
)
|
||||
|
||||
|
||||
def dip_cut_offs(gains: List[float], peak_gain: float,
|
||||
attn: float = 3.0) -> Tuple[int, int]:
|
||||
def dip_cut_offs(
|
||||
gains: List[float], peak_gain: float, attn: float = 3.0
|
||||
) -> Tuple[int, int]:
|
||||
rng = np.where(np.array(gains) < (peak_gain - attn))[0].tolist()
|
||||
return (rng[0], rng[-1]) if rng else (math.nan, math.nan)
|
||||
|
||||
|
||||
def calculate_rolloff(s21: List[Datapoint],
|
||||
idx_1: int, idx_2: int) -> Tuple[float, float]:
|
||||
def calculate_rolloff(
|
||||
s21: List[Datapoint], idx_1: int, idx_2: int
|
||||
) -> Tuple[float, float]:
|
||||
if idx_1 == idx_2:
|
||||
return (math.nan, math.nan)
|
||||
freq_1, freq_2 = s21[idx_1].freq, s21[idx_2].freq
|
||||
|
|
|
@ -35,7 +35,8 @@ IDEAL_OPEN = complex(1, 0)
|
|||
IDEAL_LOAD = complex(0, 0)
|
||||
IDEAL_THROUGH = complex(1, 0)
|
||||
|
||||
RXP_CAL_HEADER = re.compile(r"""
|
||||
RXP_CAL_HEADER = re.compile(
|
||||
r"""
|
||||
^ \# \s+ Hz \s+
|
||||
ShortR \s+ ShortI \s+ OpenR \s+ OpenI \s+
|
||||
LoadR \s+ LoadI
|
||||
|
@ -43,9 +44,12 @@ RXP_CAL_HEADER = re.compile(r"""
|
|||
(?P<thrurefl> \s+ ThrureflR \s+ ThrureflI)?
|
||||
(?P<isolation> \s+ IsolationR \s+ IsolationI)?
|
||||
\s* $
|
||||
""", re.VERBOSE | re.IGNORECASE)
|
||||
""",
|
||||
re.VERBOSE | re.IGNORECASE,
|
||||
)
|
||||
|
||||
RXP_CAL_LINE = re.compile(r"""
|
||||
RXP_CAL_LINE = re.compile(
|
||||
r"""
|
||||
^ \s*
|
||||
(?P<freq>\d+) \s+
|
||||
(?P<shortr>[-0-9Ee.]+) \s+ (?P<shorti>[-0-9Ee.]+) \s+
|
||||
|
@ -55,7 +59,9 @@ RXP_CAL_LINE = re.compile(r"""
|
|||
( \s+ (?P<thrureflr>[-0-9Ee.]+) \s+ (?P<thrurefli>[-0-9Ee.]+))?
|
||||
( \s+ (?P<isolationr>[-0-9Ee.]+) \s+ (?P<isolationi>[-0-9Ee.]+))?
|
||||
\s* $
|
||||
""", re.VERBOSE)
|
||||
""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -63,7 +69,8 @@ logger = logging.getLogger(__name__)
|
|||
def correct_delay(d: Datapoint, delay: float, reflect: bool = False):
|
||||
mult = 2 if reflect else 1
|
||||
corr_data = d.z * cmath.exp(
|
||||
complex(0, 1) * 2 * math.pi * d.freq * delay * -1 * mult)
|
||||
complex(0, 1) * 2 * math.pi * d.freq * delay * -1 * mult
|
||||
)
|
||||
return Datapoint(d.freq, corr_data.real, corr_data.imag)
|
||||
|
||||
|
||||
|
@ -88,14 +95,16 @@ class CalData:
|
|||
|
||||
def __str__(self):
|
||||
return (
|
||||
f'{self.freq}'
|
||||
f' {self.short.real} {self.short.imag}'
|
||||
f' {self.open.real} {self.open.imag}'
|
||||
f' {self.load.real} {self.load.imag}' + (
|
||||
f' {self.through.real} {self.through.imag}'
|
||||
f' {self.thrurefl.real} {self.thrurefl.imag}'
|
||||
f' {self.isolation.real} {self.isolation.imag}'
|
||||
if self.through else ''
|
||||
f"{self.freq}"
|
||||
f" {self.short.real} {self.short.imag}"
|
||||
f" {self.open.real} {self.open.imag}"
|
||||
f" {self.load.real} {self.load.imag}"
|
||||
+ (
|
||||
f" {self.through.real} {self.through.imag}"
|
||||
f" {self.thrurefl.real} {self.thrurefl.imag}"
|
||||
f" {self.isolation.real} {self.isolation.imag}"
|
||||
if self.through
|
||||
else ""
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -138,26 +147,32 @@ class CalDataSet(UserDict):
|
|||
(
|
||||
"# Calibration data for NanoVNA-Saver\n"
|
||||
+ "\n".join([f"! {note}" for note in self.notes.splitlines()])
|
||||
+ "\n" + "# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
|
||||
+ (" ThroughR ThroughI ThrureflR"
|
||||
" ThrureflI IsolationR IsolationI\n"
|
||||
if self.complete2port() else "\n")
|
||||
+ "\n".join([
|
||||
f"{self.data.get(freq)}" for freq in self.frequencies()
|
||||
]) + "\n"
|
||||
+ "\n"
|
||||
+ "# Hz ShortR ShortI OpenR OpenI LoadR LoadI"
|
||||
+ (
|
||||
" ThroughR ThroughI ThrureflR"
|
||||
" ThrureflI IsolationR IsolationI\n"
|
||||
if self.complete2port()
|
||||
else "\n"
|
||||
)
|
||||
+ "\n".join(
|
||||
[f"{self.data.get(freq)}" for freq in self.frequencies()]
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
if self.complete1port() else ""
|
||||
if self.complete1port()
|
||||
else ""
|
||||
)
|
||||
|
||||
def _append_match(self, m: re.Match, header: str,
|
||||
line_nr: int, line: str) -> None:
|
||||
def _append_match(
|
||||
self, m: re.Match, header: str, line_nr: int, line: str
|
||||
) -> None:
|
||||
cal = m.groupdict()
|
||||
columns = {
|
||||
col[:-1] for col in cal.keys() if cal[col] and col != "freq"
|
||||
}
|
||||
columns = {col[:-1] for col in cal.keys() if cal[col] and col != "freq"}
|
||||
if "through" in columns and header == "sol":
|
||||
logger.warning("Through data with sol header. %i: %s",
|
||||
line_nr, line)
|
||||
logger.warning(
|
||||
"Through data with sol header. %i: %s", line_nr, line
|
||||
)
|
||||
# fix short data (without thrurefl)
|
||||
if "thrurefl" in columns and "isolation" not in columns:
|
||||
cal["isolationr"] = cal["thrureflr"]
|
||||
|
@ -166,11 +181,14 @@ class CalDataSet(UserDict):
|
|||
for name in columns:
|
||||
self.insert(
|
||||
name,
|
||||
Datapoint(int(cal["freq"]),
|
||||
float(cal[f"{name}r"]),
|
||||
float(cal[f"{name}i"])))
|
||||
Datapoint(
|
||||
int(cal["freq"]),
|
||||
float(cal[f"{name}r"]),
|
||||
float(cal[f"{name}i"]),
|
||||
),
|
||||
)
|
||||
|
||||
def from_str(self, text: str) -> 'CalDataSet':
|
||||
def from_str(self, text: str) -> "CalDataSet":
|
||||
# reset data
|
||||
self.notes = ""
|
||||
self.data = defaultdict(CalData)
|
||||
|
@ -185,7 +203,8 @@ class CalDataSet(UserDict):
|
|||
if m := RXP_CAL_HEADER.search(line):
|
||||
if header:
|
||||
logger.warning(
|
||||
"Duplicate header in cal data. %i: %s", i, line)
|
||||
"Duplicate header in cal data. %i: %s", i, line
|
||||
)
|
||||
header = "through" if m.group("through") else "sol"
|
||||
continue
|
||||
if not line or line.startswith("#"):
|
||||
|
@ -197,13 +216,20 @@ class CalDataSet(UserDict):
|
|||
continue
|
||||
if not header:
|
||||
logger.warning(
|
||||
"Caldata without having read header: %i: %s", i, line)
|
||||
"Caldata without having read header: %i: %s", i, line
|
||||
)
|
||||
self._append_match(m, header, line, i)
|
||||
return self
|
||||
|
||||
def insert(self, name: str, dp: Datapoint):
|
||||
if name not in {'short', 'open', 'load',
|
||||
'through', 'thrurefl', 'isolation'}:
|
||||
if name not in {
|
||||
"short",
|
||||
"open",
|
||||
"load",
|
||||
"through",
|
||||
"thrurefl",
|
||||
"isolation",
|
||||
}:
|
||||
raise KeyError(name)
|
||||
freq = dp.freq
|
||||
setattr(self.data[freq], name, (dp.z))
|
||||
|
@ -223,9 +249,7 @@ class CalDataSet(UserDict):
|
|||
yield self.get(freq)
|
||||
|
||||
def size_of(self, name: str) -> int:
|
||||
return len(
|
||||
[True for val in self.data.values() if getattr(val, name)]
|
||||
)
|
||||
return len([True for val in self.data.values() if getattr(val, name)])
|
||||
|
||||
def complete1port(self) -> bool:
|
||||
for val in self.data.values():
|
||||
|
@ -244,7 +268,6 @@ class CalDataSet(UserDict):
|
|||
|
||||
class Calibration:
|
||||
def __init__(self):
|
||||
|
||||
self.notes = []
|
||||
self.dataset = CalDataSet()
|
||||
self.cal_element = CalElement()
|
||||
|
@ -278,18 +301,30 @@ class Calibration:
|
|||
gm2 = cal.open
|
||||
gm3 = cal.load
|
||||
|
||||
denominator = (g1 * (g2 - g3) * gm1 +
|
||||
g2 * g3 * gm2 - g2 * g3 * gm3 -
|
||||
(g2 * gm2 - g3 * gm3) * g1)
|
||||
cal.e00 = - ((g2 * gm3 - g3 * gm3) * g1 * gm2 -
|
||||
(g2 * g3 * gm2 - g2 * g3 * gm3 -
|
||||
(g3 * gm2 - g2 * gm3) * g1) * gm1
|
||||
) / denominator
|
||||
cal.e11 = ((g2 - g3) * gm1 - g1 * (gm2 - gm3) +
|
||||
g3 * gm2 - g2 * gm3) / denominator
|
||||
cal.delta_e = - ((g1 * (gm2 - gm3) - g2 * gm2 + g3 *
|
||||
gm3) * gm1 + (g2 * gm3 - g3 * gm3) *
|
||||
gm2) / denominator
|
||||
denominator = (
|
||||
g1 * (g2 - g3) * gm1
|
||||
+ g2 * g3 * gm2
|
||||
- g2 * g3 * gm3
|
||||
- (g2 * gm2 - g3 * gm3) * g1
|
||||
)
|
||||
cal.e00 = (
|
||||
-(
|
||||
(g2 * gm3 - g3 * gm3) * g1 * gm2
|
||||
- (g2 * g3 * gm2 - g2 * g3 * gm3 - (g3 * gm2 - g2 * gm3) * g1)
|
||||
* gm1
|
||||
)
|
||||
/ denominator
|
||||
)
|
||||
cal.e11 = (
|
||||
(g2 - g3) * gm1 - g1 * (gm2 - gm3) + g3 * gm2 - g2 * gm3
|
||||
) / denominator
|
||||
cal.delta_e = (
|
||||
-(
|
||||
(g1 * (gm2 - gm3) - g2 * gm2 + g3 * gm3) * gm1
|
||||
+ (g2 * gm3 - g3 * gm3) * gm2
|
||||
)
|
||||
/ denominator
|
||||
)
|
||||
|
||||
def _calc_port_2(self, freq: int, cal: CalData):
|
||||
gt = self.gamma_through(freq)
|
||||
|
@ -301,18 +336,16 @@ class Calibration:
|
|||
|
||||
cal.e30 = cal.isolation
|
||||
cal.e10e01 = cal.e00 * cal.e11 - cal.delta_e
|
||||
cal.e22 = gm7 / (
|
||||
gm7 * cal.e11 * gt ** 2 + cal.e10e01 * gt ** 2)
|
||||
cal.e10e32 = (gm4 - gm6) * (
|
||||
1 - cal.e11 * cal.e22 * gt ** 2) / gt
|
||||
cal.e22 = gm7 / (gm7 * cal.e11 * gt**2 + cal.e10e01 * gt**2)
|
||||
cal.e10e32 = (gm4 - gm6) * (1 - cal.e11 * cal.e22 * gt**2) / gt
|
||||
|
||||
def calc_corrections(self):
|
||||
if not self.isValid1Port():
|
||||
logger.warning(
|
||||
"Tried to calibrate from insufficient data.")
|
||||
logger.warning("Tried to calibrate from insufficient data.")
|
||||
raise ValueError(
|
||||
"All of short, open and load calibration steps"
|
||||
"must be completed for calibration to be applied.")
|
||||
"must be completed for calibration to be applied."
|
||||
)
|
||||
logger.debug("Calculating calibration for %d points.", self.size())
|
||||
|
||||
for freq, caldata in self.dataset.items():
|
||||
|
@ -324,10 +357,12 @@ class Calibration:
|
|||
self.isCalculated = False
|
||||
logger.error(
|
||||
"Division error - did you use the same measurement"
|
||||
" for two of short, open and load?")
|
||||
" for two of short, open and load?"
|
||||
)
|
||||
raise ValueError(
|
||||
f"Two of short, open and load returned the same"
|
||||
f" values at frequency {freq}Hz.") from exc
|
||||
f" values at frequency {freq}Hz."
|
||||
) from exc
|
||||
|
||||
self.gen_interpolation()
|
||||
self.isCalculated = True
|
||||
|
@ -338,25 +373,47 @@ class Calibration:
|
|||
return IDEAL_SHORT
|
||||
logger.debug("Using short calibration set values.")
|
||||
cal_element = self.cal_element
|
||||
Zsp = complex(0.0, 2.0 * math.pi * freq * (
|
||||
cal_element.short_l0 + cal_element.short_l1 * freq +
|
||||
cal_element.short_l2 * freq**2 + cal_element.short_l3 * freq**3))
|
||||
Zsp = complex(
|
||||
0.0,
|
||||
2.0
|
||||
* math.pi
|
||||
* freq
|
||||
* (
|
||||
cal_element.short_l0
|
||||
+ cal_element.short_l1 * freq
|
||||
+ cal_element.short_l2 * freq**2
|
||||
+ cal_element.short_l3 * freq**3
|
||||
),
|
||||
)
|
||||
# Referencing https://arxiv.org/pdf/1606.02446.pdf (18) - (21)
|
||||
return (Zsp / 50.0 - 1.0) / (Zsp / 50.0 + 1.0) * cmath.exp(
|
||||
complex(0.0,
|
||||
-4.0 * math.pi * freq * cal_element.short_length))
|
||||
return (
|
||||
(Zsp / 50.0 - 1.0)
|
||||
/ (Zsp / 50.0 + 1.0)
|
||||
* cmath.exp(
|
||||
complex(0.0, -4.0 * math.pi * freq * cal_element.short_length)
|
||||
)
|
||||
)
|
||||
|
||||
def gamma_open(self, freq: int) -> complex:
|
||||
if self.cal_element.open_is_ideal:
|
||||
return IDEAL_OPEN
|
||||
logger.debug("Using open calibration set values.")
|
||||
cal_element = self.cal_element
|
||||
Zop = complex(0.0, 2.0 * math.pi * freq * (
|
||||
cal_element.open_c0 + cal_element.open_c1 * freq +
|
||||
cal_element.open_c2 * freq**2 + cal_element.open_c3 * freq**3))
|
||||
Zop = complex(
|
||||
0.0,
|
||||
2.0
|
||||
* math.pi
|
||||
* freq
|
||||
* (
|
||||
cal_element.open_c0
|
||||
+ cal_element.open_c1 * freq
|
||||
+ cal_element.open_c2 * freq**2
|
||||
+ cal_element.open_c3 * freq**3
|
||||
),
|
||||
)
|
||||
return ((1.0 - 50.0 * Zop) / (1.0 + 50.0 * Zop)) * cmath.exp(
|
||||
complex(0.0,
|
||||
-4.0 * math.pi * freq * cal_element.open_length))
|
||||
complex(0.0, -4.0 * math.pi * freq * cal_element.open_length)
|
||||
)
|
||||
|
||||
def gamma_load(self, freq: int) -> complex:
|
||||
if self.cal_element.load_is_ideal:
|
||||
|
@ -367,11 +424,17 @@ class Calibration:
|
|||
if cal_element.load_c > 0.0:
|
||||
Zl = cal_element.load_r / complex(
|
||||
1.0,
|
||||
2.0 * cal_element.load_r * math.pi * freq * cal_element.load_c)
|
||||
2.0 * cal_element.load_r * math.pi * freq * cal_element.load_c,
|
||||
)
|
||||
if cal_element.load_l > 0.0:
|
||||
Zl = Zl + complex(0.0, 2 * math.pi * freq * cal_element.load_l)
|
||||
return (Zl / 50.0 - 1.0) / (Zl / 50.0 + 1.0) * cmath.exp(
|
||||
complex(0.0, -4 * math.pi * freq * cal_element.load_length))
|
||||
return (
|
||||
(Zl / 50.0 - 1.0)
|
||||
/ (Zl / 50.0 + 1.0)
|
||||
* cmath.exp(
|
||||
complex(0.0, -4 * math.pi * freq * cal_element.load_length)
|
||||
)
|
||||
)
|
||||
|
||||
def gamma_through(self, freq: int) -> complex:
|
||||
if self.cal_element.through_is_ideal:
|
||||
|
@ -379,59 +442,103 @@ class Calibration:
|
|||
logger.debug("Using through calibration set values.")
|
||||
cal_element = self.cal_element
|
||||
return cmath.exp(
|
||||
complex(0.0, -2.0 * math.pi * cal_element.through_length * freq))
|
||||
complex(0.0, -2.0 * math.pi * cal_element.through_length * freq)
|
||||
)
|
||||
|
||||
def gen_interpolation(self):
|
||||
(freq, e00, e11, delta_e, e10e01, e30, e22, e10e32) = zip(*[
|
||||
(c.freq, c.e00, c.e11, c.delta_e, c.e10e01, c.e30, c.e22, c.e10e32)
|
||||
for c in self.dataset.values()])
|
||||
(freq, e00, e11, delta_e, e10e01, e30, e22, e10e32) = zip(
|
||||
*[
|
||||
(
|
||||
c.freq,
|
||||
c.e00,
|
||||
c.e11,
|
||||
c.delta_e,
|
||||
c.e10e01,
|
||||
c.e30,
|
||||
c.e22,
|
||||
c.e10e32,
|
||||
)
|
||||
for c in self.dataset.values()
|
||||
]
|
||||
)
|
||||
|
||||
self.interp = {
|
||||
"e00": interp1d(freq, e00,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(e00[0], e00[-1])),
|
||||
"e11": interp1d(freq, e11,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(e11[0], e11[-1])),
|
||||
"delta_e": interp1d(freq, delta_e,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(delta_e[0], delta_e[-1])),
|
||||
"e10e01": interp1d(freq, e10e01,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(e10e01[0], e10e01[-1])),
|
||||
"e30": interp1d(freq, e30,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(e30[0], e30[-1])),
|
||||
"e22": interp1d(freq, e22,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(e22[0], e22[-1])),
|
||||
"e10e32": interp1d(freq, e10e32,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(e10e32[0], e10e32[-1])),
|
||||
"e00": interp1d(
|
||||
freq,
|
||||
e00,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(e00[0], e00[-1]),
|
||||
),
|
||||
"e11": interp1d(
|
||||
freq,
|
||||
e11,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(e11[0], e11[-1]),
|
||||
),
|
||||
"delta_e": interp1d(
|
||||
freq,
|
||||
delta_e,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(delta_e[0], delta_e[-1]),
|
||||
),
|
||||
"e10e01": interp1d(
|
||||
freq,
|
||||
e10e01,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(e10e01[0], e10e01[-1]),
|
||||
),
|
||||
"e30": interp1d(
|
||||
freq,
|
||||
e30,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(e30[0], e30[-1]),
|
||||
),
|
||||
"e22": interp1d(
|
||||
freq,
|
||||
e22,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(e22[0], e22[-1]),
|
||||
),
|
||||
"e10e32": interp1d(
|
||||
freq,
|
||||
e10e32,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(e10e32[0], e10e32[-1]),
|
||||
),
|
||||
}
|
||||
|
||||
def correct11(self, dp: Datapoint):
|
||||
i = self.interp
|
||||
s11 = (dp.z - i["e00"](dp.freq)) / (
|
||||
(dp.z * i["e11"](dp.freq)) - i["delta_e"](dp.freq))
|
||||
(dp.z * i["e11"](dp.freq)) - i["delta_e"](dp.freq)
|
||||
)
|
||||
return Datapoint(dp.freq, s11.real, s11.imag)
|
||||
|
||||
def correct21(self, dp: Datapoint, dp11: Datapoint):
|
||||
i = self.interp
|
||||
s21 = (dp.z - i["e30"](dp.freq)) / i["e10e32"](dp.freq)
|
||||
s21 = s21 * (i["e10e01"](dp.freq) / (i["e11"](dp.freq)
|
||||
* dp11.z - i["delta_e"](dp.freq)))
|
||||
s21 = s21 * (
|
||||
i["e10e01"](dp.freq)
|
||||
/ (i["e11"](dp.freq) * dp11.z - i["delta_e"](dp.freq))
|
||||
)
|
||||
return Datapoint(dp.freq, s21.real, s21.imag)
|
||||
|
||||
def save(self, filename: str):
|
||||
self.dataset.notes = "\n".join(self.notes)
|
||||
if not self.isValid1Port():
|
||||
raise ValueError("Not a valid calibration")
|
||||
with open(filename, mode="w", encoding='utf-8') as calfile:
|
||||
with open(filename, mode="w", encoding="utf-8") as calfile:
|
||||
calfile.write(str(self.dataset))
|
||||
|
||||
def load(self, filename):
|
||||
self.source = os.path.basename(filename)
|
||||
with open(filename, encoding='utf-8') as calfile:
|
||||
with open(filename, encoding="utf-8") as calfile:
|
||||
self.dataset = CalDataSet().from_str(calfile.read())
|
||||
self.notes = self.dataset.notes.splitlines()
|
||||
|
|
|
@ -61,20 +61,24 @@ class CombinedLogMagChart(LogMagChart):
|
|||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(int(self.dim.width // 2) - 20,
|
||||
15,
|
||||
f"{self.name} {self.name_unit}")
|
||||
qp.drawText(
|
||||
int(self.dim.width // 2) - 20, 15, f"{self.name} {self.name_unit}"
|
||||
)
|
||||
qp.drawText(10, 15, "S11")
|
||||
qp.drawText(self.leftMargin + self.dim.width - 8, 15, "S21")
|
||||
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,
|
||||
self.topMargin + self.dim.height)
|
||||
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,
|
||||
self.topMargin + self.dim.height,
|
||||
)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data11) == 0 and len(self.reference11) == 0:
|
||||
|
@ -117,8 +121,12 @@ class CombinedLogMagChart(LogMagChart):
|
|||
pen = QtGui.QPen(c)
|
||||
pen.setWidth(2)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(self.leftMargin + self.dim.width - 20, 9,
|
||||
self.leftMargin + self.dim.width - 15, 9)
|
||||
qp.drawLine(
|
||||
self.leftMargin + self.dim.width - 20,
|
||||
9,
|
||||
self.leftMargin + self.dim.width - 15,
|
||||
9,
|
||||
)
|
||||
|
||||
if self.reference11:
|
||||
c = QtGui.QColor(Chart.color.reference)
|
||||
|
@ -132,8 +140,12 @@ class CombinedLogMagChart(LogMagChart):
|
|||
pen = QtGui.QPen(c)
|
||||
pen.setWidth(2)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(self.leftMargin + self.dim.width - 20, 14,
|
||||
self.leftMargin + self.dim.width - 15, 14)
|
||||
qp.drawLine(
|
||||
self.leftMargin + self.dim.width - 20,
|
||||
14,
|
||||
self.leftMargin + self.dim.width - 15,
|
||||
14,
|
||||
)
|
||||
|
||||
self.drawData(qp, self.data11, Chart.color.sweep)
|
||||
self.drawData(qp, self.data21, Chart.color.sweep_secondary)
|
||||
|
|
|
@ -36,13 +36,16 @@ logger = logging.getLogger(__name__)
|
|||
class ChartColors: # pylint: disable=too-many-instance-attributes
|
||||
background: QColor = field(default_factory=lambda: QColor(QtCore.Qt.white))
|
||||
foreground: QColor = field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray)
|
||||
)
|
||||
reference: QColor = field(default_factory=lambda: QColor(0, 0, 255, 64))
|
||||
reference_secondary: QColor = field(
|
||||
default_factory=lambda: QColor(0, 0, 192, 48))
|
||||
default_factory=lambda: QColor(0, 0, 192, 48)
|
||||
)
|
||||
sweep: QColor = field(default_factory=lambda: QColor(QtCore.Qt.darkYellow))
|
||||
sweep_secondary: QColor = field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkMagenta))
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkMagenta)
|
||||
)
|
||||
swr: QColor = field(default_factory=lambda: QColor(255, 0, 0, 128))
|
||||
text: QColor = field(default_factory=lambda: QColor(QtCore.Qt.black))
|
||||
bands: QColor = field(default_factory=lambda: QColor(128, 128, 128, 48))
|
||||
|
@ -97,8 +100,7 @@ class ChartMarker(QtWidgets.QWidget):
|
|||
|
||||
if text and Defaults.cfg.chart.marker_label:
|
||||
text_width = self.qp.fontMetrics().horizontalAdvance(text)
|
||||
self.qp.drawText(x - int(text_width // 2),
|
||||
y - 3 - offset, text)
|
||||
self.qp.drawText(x - int(text_width // 2), y - 3 - offset, text)
|
||||
|
||||
|
||||
class Chart(QtWidgets.QWidget):
|
||||
|
@ -109,7 +111,7 @@ class Chart(QtWidgets.QWidget):
|
|||
def __init__(self, name):
|
||||
super().__init__()
|
||||
self.name = name
|
||||
self.sweepTitle = ''
|
||||
self.sweepTitle = ""
|
||||
|
||||
self.leftMargin = 30
|
||||
self.rightMargin = 20
|
||||
|
@ -130,7 +132,8 @@ class Chart(QtWidgets.QWidget):
|
|||
|
||||
self.action_popout = QtWidgets.QAction("Popout chart")
|
||||
self.action_popout.triggered.connect(
|
||||
lambda: self.popoutRequested.emit(self))
|
||||
lambda: self.popoutRequested.emit(self)
|
||||
)
|
||||
self.addAction(self.action_popout)
|
||||
|
||||
self.action_save_screenshot = QtWidgets.QAction("Save image")
|
||||
|
@ -230,7 +233,9 @@ class Chart(QtWidgets.QWidget):
|
|||
self.zoomTo(
|
||||
self.dragbox.pos_start[0],
|
||||
self.dragbox.pos_start[1],
|
||||
a0.x(), a0.y())
|
||||
a0.x(),
|
||||
a0.y(),
|
||||
)
|
||||
self.dragbox.state = False
|
||||
self.dragbox.pos = (-1, -1)
|
||||
self.dragbox.pos_start = (0, 0)
|
||||
|
@ -262,7 +267,7 @@ class Chart(QtWidgets.QWidget):
|
|||
int(self.leftMargin + ratio_x * factor_x),
|
||||
int(self.topMargin + ratio_y * factor_y),
|
||||
int(self.leftMargin + self.dim.width - (1 - ratio_x) * factor_x),
|
||||
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y)
|
||||
int(self.topMargin + self.dim.height - (1 - ratio_y) * factor_y),
|
||||
)
|
||||
a0.accept()
|
||||
|
||||
|
@ -272,8 +277,10 @@ class Chart(QtWidgets.QWidget):
|
|||
def saveScreenshot(self):
|
||||
logger.info("Saving %s to file...", self.name)
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
parent=self, caption="Save image",
|
||||
filter="PNG (*.png);;All files (*.*)")
|
||||
parent=self,
|
||||
caption="Save image",
|
||||
filter="PNG (*.png);;All files (*.*)",
|
||||
)
|
||||
|
||||
logger.debug("Filename: %s", filename)
|
||||
if not filename:
|
||||
|
@ -314,9 +321,9 @@ class Chart(QtWidgets.QWidget):
|
|||
self.update()
|
||||
|
||||
@staticmethod
|
||||
def drawMarker(x: int, y: int,
|
||||
qp: QtGui.QPainter, color: QtGui.QColor,
|
||||
number: int = 0):
|
||||
def drawMarker(
|
||||
x: int, y: int, qp: QtGui.QPainter, color: QtGui.QColor, number: int = 0
|
||||
):
|
||||
cmarker = ChartMarker(qp)
|
||||
cmarker.draw(x, y, color, f"{number}")
|
||||
|
||||
|
|
|
@ -25,9 +25,12 @@ from PyQt5 import QtWidgets, QtGui, QtCore
|
|||
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Formatting import (
|
||||
parse_frequency, parse_value,
|
||||
format_frequency_chart, format_frequency_chart_2,
|
||||
format_y_axis)
|
||||
parse_frequency,
|
||||
parse_value,
|
||||
format_frequency_chart,
|
||||
format_frequency_chart_2,
|
||||
format_y_axis,
|
||||
)
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.SITools import Format, Value
|
||||
|
||||
|
@ -35,7 +38,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class FrequencyChart(Chart):
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self.maxFrequency = 100000000
|
||||
|
@ -79,11 +81,13 @@ class FrequencyChart(Chart):
|
|||
self.action_automatic.setCheckable(True)
|
||||
self.action_automatic.setChecked(True)
|
||||
self.action_automatic.changed.connect(
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||
)
|
||||
self.action_fixed_span = QtWidgets.QAction("Fixed span")
|
||||
self.action_fixed_span.setCheckable(True)
|
||||
self.action_fixed_span.changed.connect(
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||
)
|
||||
mode_group.addAction(self.action_automatic)
|
||||
mode_group.addAction(self.action_fixed_span)
|
||||
self.x_menu.addAction(self.action_automatic)
|
||||
|
@ -91,11 +95,13 @@ class FrequencyChart(Chart):
|
|||
self.x_menu.addSeparator()
|
||||
|
||||
self.action_set_fixed_start = QtWidgets.QAction(
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_start.triggered.connect(self.setMinimumFrequency)
|
||||
|
||||
self.action_set_fixed_stop = QtWidgets.QAction(
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_stop.triggered.connect(self.setMaximumFrequency)
|
||||
|
||||
self.x_menu.addAction(self.action_set_fixed_start)
|
||||
|
@ -110,9 +116,11 @@ class FrequencyChart(Chart):
|
|||
frequency_mode_group.addAction(self.action_set_linear_x)
|
||||
frequency_mode_group.addAction(self.action_set_logarithmic_x)
|
||||
self.action_set_linear_x.triggered.connect(
|
||||
lambda: self.setLogarithmicX(False))
|
||||
lambda: self.setLogarithmicX(False)
|
||||
)
|
||||
self.action_set_logarithmic_x.triggered.connect(
|
||||
lambda: self.setLogarithmicX(True))
|
||||
lambda: self.setLogarithmicX(True)
|
||||
)
|
||||
self.action_set_linear_x.setChecked(True)
|
||||
self.x_menu.addAction(self.action_set_linear_x)
|
||||
self.x_menu.addAction(self.action_set_logarithmic_x)
|
||||
|
@ -122,11 +130,13 @@ class FrequencyChart(Chart):
|
|||
self.y_action_automatic.setCheckable(True)
|
||||
self.y_action_automatic.setChecked(True)
|
||||
self.y_action_automatic.changed.connect(
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||
)
|
||||
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
|
||||
self.y_action_fixed_span.setCheckable(True)
|
||||
self.y_action_fixed_span.changed.connect(
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||
)
|
||||
mode_group = QtWidgets.QActionGroup(self)
|
||||
mode_group.addAction(self.y_action_automatic)
|
||||
mode_group.addAction(self.y_action_fixed_span)
|
||||
|
@ -135,11 +145,13 @@ class FrequencyChart(Chart):
|
|||
self.y_menu.addSeparator()
|
||||
|
||||
self.action_set_fixed_minimum = QtWidgets.QAction(
|
||||
f"Minimum ({self.minDisplayValue})")
|
||||
f"Minimum ({self.minDisplayValue})"
|
||||
)
|
||||
self.action_set_fixed_minimum.triggered.connect(self.setMinimumValue)
|
||||
|
||||
self.action_set_fixed_maximum = QtWidgets.QAction(
|
||||
f"Maximum ({self.maxDisplayValue})")
|
||||
f"Maximum ({self.maxDisplayValue})"
|
||||
)
|
||||
self.action_set_fixed_maximum.triggered.connect(self.setMaximumValue)
|
||||
|
||||
self.y_menu.addAction(self.action_set_fixed_maximum)
|
||||
|
@ -155,9 +167,11 @@ class FrequencyChart(Chart):
|
|||
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))
|
||||
lambda: self.setLogarithmicY(False)
|
||||
)
|
||||
self.action_set_logarithmic_y.triggered.connect(
|
||||
lambda: self.setLogarithmicY(True))
|
||||
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)
|
||||
|
@ -168,16 +182,21 @@ class FrequencyChart(Chart):
|
|||
self.menu.addAction(self.action_save_screenshot)
|
||||
self.action_popout = QtWidgets.QAction("Popout chart")
|
||||
self.action_popout.triggered.connect(
|
||||
lambda: self.popoutRequested.emit(self))
|
||||
lambda: self.popoutRequested.emit(self)
|
||||
)
|
||||
self.menu.addAction(self.action_popout)
|
||||
self.setFocusPolicy(QtCore.Qt.ClickFocus)
|
||||
|
||||
self.setMinimumSize(
|
||||
self.dim.width + self.rightMargin + self.leftMargin,
|
||||
self.dim.height + self.topMargin + self.bottomMargin)
|
||||
self.dim.height + self.topMargin + self.bottomMargin,
|
||||
)
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding))
|
||||
QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
)
|
||||
)
|
||||
pal = QtGui.QPalette()
|
||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
||||
self.setPalette(pal)
|
||||
|
@ -197,13 +216,17 @@ class FrequencyChart(Chart):
|
|||
|
||||
def contextMenuEvent(self, event):
|
||||
self.action_set_fixed_start.setText(
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_stop.setText(
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_minimum.setText(
|
||||
f"Minimum ({self.minDisplayValue})")
|
||||
f"Minimum ({self.minDisplayValue})"
|
||||
)
|
||||
self.action_set_fixed_maximum.setText(
|
||||
f"Maximum ({self.maxDisplayValue})")
|
||||
f"Maximum ({self.maxDisplayValue})"
|
||||
)
|
||||
|
||||
if self.fixedSpan:
|
||||
self.action_fixed_span.setChecked(True)
|
||||
|
@ -242,8 +265,11 @@ class FrequencyChart(Chart):
|
|||
|
||||
def setMinimumFrequency(self):
|
||||
min_freq_str, selected = QtWidgets.QInputDialog.getText(
|
||||
self, "Start frequency",
|
||||
"Set start frequency", text=str(self.minFrequency))
|
||||
self,
|
||||
"Start frequency",
|
||||
"Set start frequency",
|
||||
text=str(self.minFrequency),
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
span = abs(self.maxFrequency - self.minFrequency)
|
||||
|
@ -258,8 +284,11 @@ class FrequencyChart(Chart):
|
|||
|
||||
def setMaximumFrequency(self):
|
||||
max_freq_str, selected = QtWidgets.QInputDialog.getText(
|
||||
self, "Stop frequency",
|
||||
"Set stop frequency", text=str(self.maxFrequency))
|
||||
self,
|
||||
"Stop frequency",
|
||||
"Set stop frequency",
|
||||
text=str(self.maxFrequency),
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
span = abs(self.maxFrequency - self.minFrequency)
|
||||
|
@ -274,9 +303,11 @@ class FrequencyChart(Chart):
|
|||
|
||||
def setMinimumValue(self):
|
||||
text, selected = QtWidgets.QInputDialog.getText(
|
||||
self, "Minimum value",
|
||||
self,
|
||||
"Minimum value",
|
||||
"Set minimum value",
|
||||
text=format_y_axis(self.minDisplayValue, self.name_unit))
|
||||
text=format_y_axis(self.minDisplayValue, self.name_unit),
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
min_val = parse_value(text)
|
||||
|
@ -292,9 +323,11 @@ class FrequencyChart(Chart):
|
|||
|
||||
def setMaximumValue(self):
|
||||
text, selected = QtWidgets.QInputDialog.getText(
|
||||
self, "Maximum value",
|
||||
self,
|
||||
"Maximum value",
|
||||
"Set maximum value",
|
||||
text=format_y_axis(self.maxDisplayValue, self.name_unit))
|
||||
text=format_y_axis(self.maxDisplayValue, self.name_unit),
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
max_val = parse_value(text)
|
||||
|
@ -323,18 +356,21 @@ class FrequencyChart(Chart):
|
|||
if self.logarithmicX:
|
||||
span = math.log(self.fstop) - math.log(self.fstart)
|
||||
return self.leftMargin + round(
|
||||
self.dim.width * (math.log(d.freq) -
|
||||
math.log(self.fstart)) / span)
|
||||
self.dim.width
|
||||
* (math.log(d.freq) - math.log(self.fstart))
|
||||
/ span
|
||||
)
|
||||
return self.leftMargin + round(
|
||||
self.dim.width * (d.freq - self.fstart) / span)
|
||||
self.dim.width * (d.freq - self.fstart) / span
|
||||
)
|
||||
return math.floor(self.width() / 2)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
try:
|
||||
return (
|
||||
self.topMargin + round(
|
||||
(self.maxValue - self.value_function(d)) /
|
||||
self.span * self.dim.height)
|
||||
return self.topMargin + round(
|
||||
(self.maxValue - self.value_function(d))
|
||||
/ self.span
|
||||
* self.dim.height
|
||||
)
|
||||
except ValueError:
|
||||
return self.topMargin
|
||||
|
@ -410,9 +446,12 @@ class FrequencyChart(Chart):
|
|||
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
|
||||
dx = self.dragbox.move_x - a0.x()
|
||||
dy = self.dragbox.move_y - a0.y()
|
||||
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
|
||||
self.leftMargin + self.dim.width + dx,
|
||||
self.topMargin + self.dim.height + dy)
|
||||
self.zoomTo(
|
||||
self.leftMargin + dx,
|
||||
self.topMargin + dy,
|
||||
self.leftMargin + self.dim.width + dx,
|
||||
self.topMargin + self.dim.height + dy,
|
||||
)
|
||||
|
||||
self.dragbox.move_x = a0.x()
|
||||
self.dragbox.move_y = a0.y()
|
||||
|
@ -436,10 +475,10 @@ class FrequencyChart(Chart):
|
|||
m.setFrequency(str(f))
|
||||
|
||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||
self.dim.width = (
|
||||
a0.size().width() - self.rightMargin - self.leftMargin)
|
||||
self.dim.width = a0.size().width() - self.rightMargin - self.leftMargin
|
||||
self.dim.height = (
|
||||
a0.size().height() - self.bottomMargin - self.topMargin)
|
||||
a0.size().height() - self.bottomMargin - self.topMargin
|
||||
)
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, _: QtGui.QPaintEvent) -> None:
|
||||
|
@ -452,24 +491,30 @@ class FrequencyChart(Chart):
|
|||
qp.end()
|
||||
|
||||
def _data_oob(self, data: List[Datapoint]) -> bool:
|
||||
return (data[0].freq > self.fstop or self.data[-1].freq < self.fstart)
|
||||
return data[0].freq > self.fstop or self.data[-1].freq < self.fstart
|
||||
|
||||
def _check_frequency_boundaries(self, qp: QtGui.QPainter):
|
||||
if (self.data and self._data_oob(self.data) and
|
||||
(not self.reference or self._data_oob(self.reference))):
|
||||
if (
|
||||
self.data
|
||||
and self._data_oob(self.data)
|
||||
and (not self.reference or self._data_oob(self.reference))
|
||||
):
|
||||
# Data outside frequency range
|
||||
qp.setBackgroundMode(QtCore.Qt.OpaqueMode)
|
||||
qp.setBackground(Chart.color.background)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(self.leftMargin + int(self.dim.width // 2) - 70,
|
||||
self.topMargin + int(self.dim.height // 2) - 20,
|
||||
"Data outside frequency span")
|
||||
qp.drawText(
|
||||
self.leftMargin + int(self.dim.width // 2) - 70,
|
||||
self.topMargin + int(self.dim.height // 2) - 20,
|
||||
"Data outside frequency span",
|
||||
)
|
||||
|
||||
def drawDragbog(self, qp: QtGui.QPainter):
|
||||
dashed_pen = QtGui.QPen(Chart.color.foreground, 1, QtCore.Qt.DashLine)
|
||||
qp.setPen(dashed_pen)
|
||||
top_left = QtCore.QPoint(
|
||||
self.dragbox.pos_start[0], self.dragbox.pos_start[1])
|
||||
self.dragbox.pos_start[0], self.dragbox.pos_start[1]
|
||||
)
|
||||
bottom_right = QtCore.QPoint(self.dragbox.pos[0], self.dragbox.pos[1])
|
||||
rect = QtCore.QRect(top_left, bottom_right)
|
||||
qp.drawRect(rect)
|
||||
|
@ -481,14 +526,18 @@ class FrequencyChart(Chart):
|
|||
headline += f" ({self.name_unit})"
|
||||
qp.drawText(3, 15, headline)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin,
|
||||
20,
|
||||
self.leftMargin,
|
||||
self.topMargin + self.dim.height + 5)
|
||||
qp.drawLine(self.leftMargin - 5,
|
||||
self.topMargin + self.dim.height,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height)
|
||||
qp.drawLine(
|
||||
self.leftMargin,
|
||||
20,
|
||||
self.leftMargin,
|
||||
self.topMargin + self.dim.height + 5,
|
||||
)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin + self.dim.height,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height,
|
||||
)
|
||||
self.drawTitle(qp)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
|
@ -514,7 +563,8 @@ class FrequencyChart(Chart):
|
|||
if span == 0:
|
||||
logger.info(
|
||||
"Span is zero for %s-Chart, setting to a small value.",
|
||||
self.name)
|
||||
self.name,
|
||||
)
|
||||
span = 1e-15
|
||||
self.span = span
|
||||
|
||||
|
@ -522,23 +572,30 @@ class FrequencyChart(Chart):
|
|||
fmt = Format(max_nr_digits=1)
|
||||
for i in range(target_ticks):
|
||||
val = min_value + (i / target_ticks) * span
|
||||
y = self.topMargin + \
|
||||
round((self.maxValue - val) / self.span * self.dim.height)
|
||||
y = self.topMargin + round(
|
||||
(self.maxValue - val) / self.span * self.dim.height
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
if val != min_value:
|
||||
valstr = str(Value(val, fmt=fmt))
|
||||
qp.drawText(3, y + 3, valstr)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
||||
self.leftMargin + self.dim.width, self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 4, str(Value(max_value, fmt=fmt)))
|
||||
qp.drawText(3, self.dim.height + self.topMargin,
|
||||
str(Value(min_value, fmt=fmt)))
|
||||
qp.drawText(
|
||||
3, self.dim.height + self.topMargin, str(Value(min_value, fmt=fmt))
|
||||
)
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
self.drawData(qp, self.data, Chart.color.sweep)
|
||||
|
@ -574,27 +631,31 @@ class FrequencyChart(Chart):
|
|||
else:
|
||||
my_format_frequency = format_frequency_chart_2
|
||||
|
||||
qp.drawText(self.leftMargin - 20,
|
||||
self.topMargin + self.dim.height + 15,
|
||||
my_format_frequency(self.fstart))
|
||||
qp.drawText(
|
||||
self.leftMargin - 20,
|
||||
self.topMargin + self.dim.height + 15,
|
||||
my_format_frequency(self.fstart),
|
||||
)
|
||||
|
||||
for i in range(ticks):
|
||||
x = self.leftMargin + round((i + 1) * self.dim.width / ticks)
|
||||
if self.logarithmicX:
|
||||
fspan = math.log(self.fstop) - math.log(self.fstart)
|
||||
freq = round(
|
||||
math.exp(
|
||||
((i + 1) * fspan / ticks) +
|
||||
math.log(self.fstart)))
|
||||
math.exp(((i + 1) * fspan / ticks) + math.log(self.fstart))
|
||||
)
|
||||
else:
|
||||
freq = round(fspan / ticks * (i + 1) + self.fstart)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(x, self.topMargin, x,
|
||||
self.topMargin + self.dim.height + 5)
|
||||
qp.drawLine(
|
||||
x, self.topMargin, x, self.topMargin + self.dim.height + 5
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(x - 20,
|
||||
self.topMargin + self.dim.height + 15,
|
||||
my_format_frequency(freq))
|
||||
qp.drawText(
|
||||
x - 20,
|
||||
self.topMargin + self.dim.height + 15,
|
||||
my_format_frequency(freq),
|
||||
)
|
||||
|
||||
def drawBands(self, qp, fstart, fstop):
|
||||
qp.setBrush(self.bands.color)
|
||||
|
@ -608,17 +669,24 @@ class FrequencyChart(Chart):
|
|||
# don't draw if either band not in chart or completely in band
|
||||
if start < fstart < fstop < end or end < fstart or start > fstop:
|
||||
continue
|
||||
x_start = max(self.leftMargin + 1,
|
||||
self.getXPosition(Datapoint(start, 0, 0)))
|
||||
x_stop = min(self.leftMargin + self.dim.width,
|
||||
self.getXPosition(Datapoint(end, 0, 0)))
|
||||
qp.drawRect(x_start,
|
||||
self.topMargin,
|
||||
x_stop - x_start,
|
||||
self.dim.height)
|
||||
x_start = max(
|
||||
self.leftMargin + 1, self.getXPosition(Datapoint(start, 0, 0))
|
||||
)
|
||||
x_stop = min(
|
||||
self.leftMargin + self.dim.width,
|
||||
self.getXPosition(Datapoint(end, 0, 0)),
|
||||
)
|
||||
qp.drawRect(
|
||||
x_start, self.topMargin, x_stop - x_start, self.dim.height
|
||||
)
|
||||
|
||||
def drawData(self, qp: QtGui.QPainter, data: List[Datapoint],
|
||||
color: QtGui.QColor, y_function=None):
|
||||
def drawData(
|
||||
self,
|
||||
qp: QtGui.QPainter,
|
||||
data: List[Datapoint],
|
||||
color: QtGui.QColor,
|
||||
y_function=None,
|
||||
):
|
||||
if y_function is None:
|
||||
y_function = self.getYPosition
|
||||
pen = QtGui.QPen(color)
|
||||
|
@ -643,8 +711,7 @@ class FrequencyChart(Chart):
|
|||
if self.isPlotable(prevx, prevy):
|
||||
qp.drawLine(x, y, prevx, prevy)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y, prevx, prevy)
|
||||
new_x, new_y = self.getPlotable(x, y, prevx, prevy)
|
||||
qp.drawLine(x, y, new_x, new_y)
|
||||
elif self.isPlotable(prevx, prevy):
|
||||
new_x, new_y = self.getPlotable(prevx, prevy, x, y)
|
||||
|
@ -663,13 +730,17 @@ class FrequencyChart(Chart):
|
|||
x = self.getXPosition(data[m.location])
|
||||
y = y_function(data[m.location])
|
||||
if self.isPlotable(x, y):
|
||||
self.drawMarker(x, y, qp, m.color,
|
||||
self.markers.index(m) + 1)
|
||||
self.drawMarker(
|
||||
x, y, qp, m.color, self.markers.index(m) + 1
|
||||
)
|
||||
|
||||
def isPlotable(self, x, y):
|
||||
return y is not None and x is not None and \
|
||||
self.leftMargin <= x <= self.leftMargin + self.dim.width and \
|
||||
self.topMargin <= y <= self.topMargin + self.dim.height
|
||||
return (
|
||||
y is not None
|
||||
and x is not None
|
||||
and self.leftMargin <= x <= self.leftMargin + self.dim.width
|
||||
and self.topMargin <= y <= self.topMargin + self.dim.height
|
||||
)
|
||||
|
||||
def getPlotable(self, x, y, distantx, distanty):
|
||||
p1 = np.array([x, y])
|
||||
|
@ -680,8 +751,12 @@ class FrequencyChart(Chart):
|
|||
p4 = np.array([self.leftMargin + self.dim.width, self.topMargin])
|
||||
elif distanty > self.topMargin + self.dim.height:
|
||||
p3 = np.array([self.leftMargin, self.topMargin + self.dim.height])
|
||||
p4 = np.array([self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height])
|
||||
p4 = np.array(
|
||||
[
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height,
|
||||
]
|
||||
)
|
||||
else:
|
||||
return x, y
|
||||
|
||||
|
@ -730,10 +805,14 @@ class FrequencyChart(Chart):
|
|||
m = self.getActiveMarker()
|
||||
if m is not None and a0.modifiers() == QtCore.Qt.NoModifier:
|
||||
if a0.key() in [QtCore.Qt.Key_Down, QtCore.Qt.Key_Left]:
|
||||
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
|
||||
a0.type(), QtCore.Qt.Key_Down, a0.modifiers()))
|
||||
m.frequencyInput.keyPressEvent(
|
||||
QtGui.QKeyEvent(
|
||||
a0.type(), QtCore.Qt.Key_Down, a0.modifiers()
|
||||
)
|
||||
)
|
||||
elif a0.key() in [QtCore.Qt.Key_Up, QtCore.Qt.Key_Right]:
|
||||
m.frequencyInput.keyPressEvent(QtGui.QKeyEvent(
|
||||
a0.type(), QtCore.Qt.Key_Up, a0.modifiers()))
|
||||
m.frequencyInput.keyPressEvent(
|
||||
QtGui.QKeyEvent(a0.type(), QtCore.Qt.Key_Up, a0.modifiers())
|
||||
)
|
||||
else:
|
||||
super().keyPressEvent(a0)
|
||||
|
|
|
@ -27,6 +27,7 @@ from PyQt5 import QtGui
|
|||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from .Frequency import FrequencyChart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -124,23 +125,30 @@ class GroupDelayChart(FrequencyChart):
|
|||
tickcount = math.floor(self.dim.height / 60)
|
||||
for i in range(tickcount):
|
||||
delay = min_delay + span * i / tickcount
|
||||
y = self.topMargin + \
|
||||
round((self.maxDelay - delay) / self.span * self.dim.height)
|
||||
y = self.topMargin + round(
|
||||
(self.maxDelay - delay) / self.span * self.dim.height
|
||||
)
|
||||
if delay not in {min_delay, max_delay}:
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
# TODO use format class
|
||||
digits = 0 if delay == 0 else max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(delay)))))
|
||||
digits = (
|
||||
0
|
||||
if delay == 0
|
||||
else max(0, min(2, math.floor(3 - math.log10(abs(delay)))))
|
||||
)
|
||||
delaystr = str(round(delay, digits if digits != 0 else None))
|
||||
qp.drawText(3, y + 3, delaystr)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
|
||||
qp.drawLine(self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 5, str(max_delay))
|
||||
qp.drawText(3, self.dim.height + self.topMargin, str(min_delay))
|
||||
|
@ -153,15 +161,20 @@ class GroupDelayChart(FrequencyChart):
|
|||
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
self.draw_data(qp, Chart.color.sweep,
|
||||
self.data, self.groupDelay)
|
||||
self.draw_data(qp, Chart.color.reference,
|
||||
self.reference, self.groupDelayReference)
|
||||
self.draw_data(qp, Chart.color.sweep, self.data, self.groupDelay)
|
||||
self.draw_data(
|
||||
qp, Chart.color.reference, self.reference, self.groupDelayReference
|
||||
)
|
||||
|
||||
self.drawMarkers(qp)
|
||||
|
||||
def draw_data(self, qp: QtGui.QPainter, color: QtGui.QColor,
|
||||
data: List[Datapoint], delay: List[Datapoint]):
|
||||
def draw_data(
|
||||
self,
|
||||
qp: QtGui.QPainter,
|
||||
color: QtGui.QColor,
|
||||
data: List[Datapoint],
|
||||
delay: List[Datapoint],
|
||||
):
|
||||
pen = QtGui.QPen(color)
|
||||
pen.setWidth(self.dim.point)
|
||||
line_pen = QtGui.QPen(color)
|
||||
|
@ -200,7 +213,8 @@ class GroupDelayChart(FrequencyChart):
|
|||
|
||||
def getYPositionFromDelay(self, delay: float) -> int:
|
||||
return self.topMargin + int(
|
||||
(self.maxDelay - delay) / self.span * self.dim.height)
|
||||
(self.maxDelay - delay) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
|
|
|
@ -115,8 +115,12 @@ class LogMagChart(FrequencyChart):
|
|||
self.draw_db_lines(qp, self.maxValue, self.minValue, ticks)
|
||||
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
||||
self.leftMargin + self.dim.width, self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 4, f"{self.maxValue}")
|
||||
qp.drawText(3, self.dim.height + self.topMargin, f"{self.minValue}")
|
||||
|
@ -127,14 +131,17 @@ class LogMagChart(FrequencyChart):
|
|||
for i in range(ticks.count):
|
||||
db = ticks.first + i * ticks.step
|
||||
y = self.topMargin + round(
|
||||
(maxValue - db) / self.span * self.dim.height)
|
||||
(maxValue - db) / self.span * self.dim.height
|
||||
)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
if db > minValue and db != maxValue:
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(3, y + 4,
|
||||
f"{round(db, 1)}" if ticks.step < 1 else f"{db}")
|
||||
qp.drawText(
|
||||
3, y + 4, f"{round(db, 1)}" if ticks.step < 1 else f"{db}"
|
||||
)
|
||||
|
||||
def draw_swr_markers(self, qp) -> None:
|
||||
qp.setPen(Chart.color.swr)
|
||||
|
@ -145,9 +152,9 @@ class LogMagChart(FrequencyChart):
|
|||
if self.isInverted:
|
||||
logMag = logMag * -1
|
||||
y = self.topMargin + round(
|
||||
(self.maxValue - logMag) / self.span * self.dim.height)
|
||||
qp.drawLine(self.leftMargin, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
(self.maxValue - logMag) / self.span * self.dim.height
|
||||
)
|
||||
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
|
||||
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
|
@ -155,7 +162,8 @@ class LogMagChart(FrequencyChart):
|
|||
if math.isinf(logMag):
|
||||
return self.topMargin
|
||||
return self.topMargin + int(
|
||||
(self.maxValue - logMag) / self.span * self.dim.height)
|
||||
(self.maxValue - logMag) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
|
|
|
@ -25,6 +25,7 @@ from PyQt5 import QtGui
|
|||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -78,21 +79,28 @@ class MagnitudeChart(FrequencyChart):
|
|||
target_ticks = int(self.dim.height // 60)
|
||||
for i in range(target_ticks):
|
||||
val = min_value + i / target_ticks * self.span
|
||||
y = self.topMargin + int((self.maxValue - val) / self.span
|
||||
* self.dim.height)
|
||||
y = self.topMargin + int(
|
||||
(self.maxValue - val) / self.span * self.dim.height
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
if val != min_value:
|
||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(val)))))
|
||||
vswrstr = (str(round(val)) if digits == 0 else
|
||||
str(round(val, digits)))
|
||||
vswrstr = (
|
||||
str(round(val)) if digits == 0 else str(round(val, digits))
|
||||
)
|
||||
qp.drawText(3, y + 3, vswrstr)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
||||
self.leftMargin + self.dim.width, self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 4, str(max_value))
|
||||
qp.drawText(3, self.dim.height + self.topMargin, str(min_value))
|
||||
|
@ -103,10 +111,10 @@ class MagnitudeChart(FrequencyChart):
|
|||
if vswr <= 1:
|
||||
continue
|
||||
mag = (vswr - 1) / (vswr + 1)
|
||||
y = self.topMargin + int((self.maxValue - mag) / self.span
|
||||
* self.dim.height)
|
||||
qp.drawLine(self.leftMargin, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
y = self.topMargin + int(
|
||||
(self.maxValue - mag) / self.span * self.dim.height
|
||||
)
|
||||
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
|
||||
qp.drawText(self.leftMargin + 3, y - 1, f"VSWR: {vswr}")
|
||||
|
||||
self.drawData(qp, self.data, Chart.color.sweep)
|
||||
|
@ -116,7 +124,8 @@ class MagnitudeChart(FrequencyChart):
|
|||
def getYPosition(self, d: Datapoint) -> int:
|
||||
mag = self.magnitude(d)
|
||||
return self.topMargin + int(
|
||||
(self.maxValue - mag) / self.span * self.dim.height)
|
||||
(self.maxValue - mag) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
|
|
|
@ -23,8 +23,7 @@ from typing import List
|
|||
from PyQt5 import QtGui
|
||||
|
||||
from NanoVNASaver.RFTools import Datapoint
|
||||
from NanoVNASaver.SITools import (
|
||||
Format, Value, round_ceil, round_floor)
|
||||
from NanoVNASaver.SITools import Format, Value, round_ceil, round_floor
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||
from NanoVNASaver.Charts.LogMag import LogMagChart
|
||||
|
@ -57,8 +56,10 @@ class MagnitudeZChart(FrequencyChart):
|
|||
if self.fixedValues:
|
||||
self.maxValue = self.maxDisplayValue
|
||||
self.minValue = (
|
||||
max(self.minDisplayValue, 0.01) if self.logarithmicY else
|
||||
self.minDisplayValue)
|
||||
max(self.minDisplayValue, 0.01)
|
||||
if self.logarithmicY
|
||||
else self.minDisplayValue
|
||||
)
|
||||
else:
|
||||
# Find scaling
|
||||
self.minValue = 100
|
||||
|
@ -92,15 +93,18 @@ class MagnitudeZChart(FrequencyChart):
|
|||
for i in range(horizontal_ticks):
|
||||
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width + 5, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
|
||||
)
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
|
||||
qp.drawText(3, y + 4, str(val))
|
||||
|
||||
qp.drawText(3,
|
||||
self.dim.height + self.topMargin,
|
||||
str(Value(self.minValue, fmt=fmt)))
|
||||
qp.drawText(
|
||||
3,
|
||||
self.dim.height + self.topMargin,
|
||||
str(Value(self.minValue, fmt=fmt)),
|
||||
)
|
||||
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
|
@ -116,18 +120,22 @@ class MagnitudeZChart(FrequencyChart):
|
|||
if self.logarithmicY:
|
||||
span = math.log(self.maxValue) - math.log(self.minValue)
|
||||
return self.topMargin + int(
|
||||
(math.log(self.maxValue) - math.log(mag)) /
|
||||
span * self.dim.height)
|
||||
(math.log(self.maxValue) - math.log(mag))
|
||||
/ span
|
||||
* self.dim.height
|
||||
)
|
||||
return self.topMargin + int(
|
||||
(self.maxValue - mag) / self.span * self.dim.height)
|
||||
(self.maxValue - mag) / self.span * self.dim.height
|
||||
)
|
||||
return self.topMargin
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
if self.logarithmicY:
|
||||
span = math.log(self.maxValue) - math.log(self.minValue)
|
||||
val = math.exp(math.log(self.maxValue) -
|
||||
absy * span / self.dim.height)
|
||||
val = math.exp(
|
||||
math.log(self.maxValue) - absy * span / self.dim.height
|
||||
)
|
||||
else:
|
||||
val = self.maxValue - (absy / self.dim.height * self.span)
|
||||
return [val]
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
|
@ -27,7 +26,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class MagnitudeZSeriesChart(MagnitudeZChart):
|
||||
|
||||
@staticmethod
|
||||
def magnitude(p: Datapoint) -> float:
|
||||
return abs(p.seriesImpedance())
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
# NanoVNASaver
|
||||
#
|
||||
# A python program to view and export Touchstone data from a NanoVNA
|
||||
|
@ -26,7 +25,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class MagnitudeZShuntChart(MagnitudeZChart):
|
||||
|
||||
@staticmethod
|
||||
def magnitude(p: Datapoint) -> float:
|
||||
return abs(p.shuntImpedance())
|
||||
|
|
|
@ -27,6 +27,7 @@ from NanoVNASaver.RFTools import Datapoint
|
|||
from NanoVNASaver.SITools import Format, Value
|
||||
from NanoVNASaver.Charts.Chart import Chart
|
||||
from NanoVNASaver.Charts.Frequency import FrequencyChart
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
|
@ -50,19 +51,26 @@ class PermeabilityChart(FrequencyChart):
|
|||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(self.leftMargin + 5, 15, self.name +
|
||||
" (\N{MICRO SIGN}\N{OHM SIGN} / Hz)")
|
||||
qp.drawText(
|
||||
self.leftMargin + 5,
|
||||
15,
|
||||
self.name + " (\N{MICRO SIGN}\N{OHM SIGN} / Hz)",
|
||||
)
|
||||
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)
|
||||
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):
|
||||
|
@ -121,15 +129,16 @@ class PermeabilityChart(FrequencyChart):
|
|||
for i in range(horizontal_ticks):
|
||||
y = self.topMargin + round(i * self.dim.height / horizontal_ticks)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width + 5, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
|
||||
)
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
val = Value(self.valueAtPosition(y)[0], fmt=fmt)
|
||||
qp.drawText(3, y + 4, str(val))
|
||||
|
||||
qp.drawText(3,
|
||||
self.dim.height + self.topMargin,
|
||||
str(Value(min_val, fmt=fmt)))
|
||||
qp.drawText(
|
||||
3, self.dim.height + self.topMargin, str(Value(min_val, fmt=fmt))
|
||||
)
|
||||
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
|
@ -147,8 +156,11 @@ class PermeabilityChart(FrequencyChart):
|
|||
pen.setColor(c)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(
|
||||
self.leftMargin + self.dim.width, 9,
|
||||
self.leftMargin + self.dim.width + 5, 9)
|
||||
self.leftMargin + self.dim.width,
|
||||
9,
|
||||
self.leftMargin + self.dim.width + 5,
|
||||
9,
|
||||
)
|
||||
|
||||
primary_pen.setWidth(self.dim.point)
|
||||
secondary_pen.setWidth(self.dim.point)
|
||||
|
@ -177,7 +189,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_re, prev_x, prev_y_re)
|
||||
x, y_re, prev_x, prev_y_re
|
||||
)
|
||||
qp.drawLine(x, y_re, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_re):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||
|
@ -191,7 +204,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_im, prev_x, prev_y_im)
|
||||
x, y_im, prev_x, prev_y_im
|
||||
)
|
||||
qp.drawLine(x, y_im, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_im):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||
|
@ -213,8 +227,12 @@ class PermeabilityChart(FrequencyChart):
|
|||
pen = QtGui.QPen(c)
|
||||
pen.setWidth(2)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(self.leftMargin + self.dim.width, 14,
|
||||
self.leftMargin + self.dim.width + 5, 14)
|
||||
qp.drawLine(
|
||||
self.leftMargin + self.dim.width,
|
||||
14,
|
||||
self.leftMargin + self.dim.width + 5,
|
||||
14,
|
||||
)
|
||||
|
||||
for i, reference in enumerate(self.reference):
|
||||
if reference.freq < self.fstart or reference.freq > self.fstop:
|
||||
|
@ -241,7 +259,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_re, prev_x, prev_y_re)
|
||||
x, y_re, prev_x, prev_y_re
|
||||
)
|
||||
qp.drawLine(x, y_re, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_re):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||
|
@ -255,7 +274,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_im, prev_x, prev_y_im)
|
||||
x, y_im, prev_x, prev_y_im
|
||||
)
|
||||
qp.drawLine(x, y_im, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_im):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||
|
@ -268,10 +288,8 @@ class PermeabilityChart(FrequencyChart):
|
|||
y_re = self.getReYPosition(self.data[m.location])
|
||||
y_im = self.getImYPosition(self.data[m.location])
|
||||
|
||||
self.drawMarker(x, y_re, qp, m.color,
|
||||
self.markers.index(m) + 1)
|
||||
self.drawMarker(x, y_im, qp, m.color,
|
||||
self.markers.index(m) + 1)
|
||||
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m) + 1)
|
||||
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m) + 1)
|
||||
|
||||
def getImYPosition(self, d: Datapoint) -> int:
|
||||
im = d.impedance().imag
|
||||
|
@ -283,10 +301,12 @@ class PermeabilityChart(FrequencyChart):
|
|||
else:
|
||||
return -1
|
||||
return int(
|
||||
self.topMargin + (math.log(self.max) - math.log(im)) /
|
||||
span * self.dim.height)
|
||||
return int(self.topMargin + (self.max - im) /
|
||||
self.span * self.dim.height)
|
||||
self.topMargin
|
||||
+ (math.log(self.max) - math.log(im)) / span * self.dim.height
|
||||
)
|
||||
return int(
|
||||
self.topMargin + (self.max - im) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def getReYPosition(self, d: Datapoint) -> int:
|
||||
re = d.impedance().real
|
||||
|
@ -298,10 +318,12 @@ class PermeabilityChart(FrequencyChart):
|
|||
else:
|
||||
return -1
|
||||
return int(
|
||||
self.topMargin + (math.log(self.max) - math.log(re)) /
|
||||
span * self.dim.height)
|
||||
self.topMargin
|
||||
+ (math.log(self.max) - math.log(re)) / span * self.dim.height
|
||||
)
|
||||
return int(
|
||||
self.topMargin + (self.max - re) / self.span * self.dim.height)
|
||||
self.topMargin + (self.max - re) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
|
|
|
@ -50,7 +50,8 @@ class PhaseChart(FrequencyChart):
|
|||
self.action_unwrap = QtWidgets.QAction("Unwrap")
|
||||
self.action_unwrap.setCheckable(True)
|
||||
self.action_unwrap.triggered.connect(
|
||||
lambda: self.setUnwrap(self.action_unwrap.isChecked()))
|
||||
lambda: self.setUnwrap(self.action_unwrap.isChecked())
|
||||
)
|
||||
self.y_menu.addAction(self.action_unwrap)
|
||||
|
||||
def copy(self):
|
||||
|
@ -98,24 +99,32 @@ class PhaseChart(FrequencyChart):
|
|||
for i in range(tickcount):
|
||||
angle = minAngle + span * i / tickcount
|
||||
y = self.topMargin + int(
|
||||
(self.maxAngle - angle) / self.span * self.dim.height)
|
||||
(self.maxAngle - angle) / self.span * self.dim.height
|
||||
)
|
||||
if angle not in [minAngle, maxAngle]:
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
if angle != 0:
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(angle)))))
|
||||
anglestr = str(round(angle)) if digits == 0 else str(
|
||||
round(angle, digits))
|
||||
0, min(2, math.floor(3 - math.log10(abs(angle))))
|
||||
)
|
||||
anglestr = (
|
||||
str(round(angle))
|
||||
if digits == 0
|
||||
else str(round(angle, digits))
|
||||
)
|
||||
else:
|
||||
anglestr = "0"
|
||||
qp.drawText(3, y + 3, f"{anglestr}°")
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 5, f"{maxAngle}°")
|
||||
qp.drawText(3, self.dim.height + self.topMargin, f"{minAngle}°")
|
||||
|
@ -139,7 +148,8 @@ class PhaseChart(FrequencyChart):
|
|||
else:
|
||||
angle = math.degrees(d.phase)
|
||||
return self.topMargin + int(
|
||||
(self.maxAngle - angle) / self.span * self.dim.height)
|
||||
(self.maxAngle - angle) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
|
|
|
@ -39,16 +39,25 @@ class PolarChart(SquareChart):
|
|||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
|
||||
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
|
||||
qp.drawEllipse(QtCore.QPoint(center_x, center_y),
|
||||
width_2 // 2, height_2 // 2)
|
||||
qp.drawEllipse(
|
||||
QtCore.QPoint(center_x, center_y), width_2 // 2, height_2 // 2
|
||||
)
|
||||
|
||||
qp.drawLine(center_x - width_2, center_y,
|
||||
center_x + width_2, center_y)
|
||||
qp.drawLine(center_x, center_y - height_2,
|
||||
center_x, center_y + height_2)
|
||||
qp.drawLine(center_x + width_45, center_y + height_45,
|
||||
center_x - width_45, center_y - height_45)
|
||||
qp.drawLine(center_x + width_45, center_y - height_45,
|
||||
center_x - width_45, center_y + height_45)
|
||||
qp.drawLine(center_x - width_2, center_y, center_x + width_2, center_y)
|
||||
qp.drawLine(
|
||||
center_x, center_y - height_2, center_x, center_y + height_2
|
||||
)
|
||||
qp.drawLine(
|
||||
center_x + width_45,
|
||||
center_y + height_45,
|
||||
center_x - width_45,
|
||||
center_y - height_45,
|
||||
)
|
||||
qp.drawLine(
|
||||
center_x + width_45,
|
||||
center_y - height_45,
|
||||
center_x - width_45,
|
||||
center_y + height_45,
|
||||
)
|
||||
|
||||
self.drawTitle(qp)
|
||||
|
|
|
@ -57,7 +57,7 @@ class QualityFactorChart(FrequencyChart):
|
|||
scale = 0
|
||||
if maxQ > 0:
|
||||
scale = max(scale, math.floor(math.log10(maxQ)))
|
||||
maxQ = math.ceil(maxQ / 10 ** scale) * 10 ** scale
|
||||
maxQ = math.ceil(maxQ / 10**scale) * 10**scale
|
||||
|
||||
self.minQ = self.minDisplayValue
|
||||
self.maxQ = maxQ
|
||||
|
@ -69,8 +69,9 @@ class QualityFactorChart(FrequencyChart):
|
|||
|
||||
for i in range(tickcount):
|
||||
q = self.minQ + i * self.span / tickcount
|
||||
y = self.topMargin + int((self.maxQ - q) / self.span *
|
||||
self.dim.height)
|
||||
y = self.topMargin + int(
|
||||
(self.maxQ - q) / self.span * self.dim.height
|
||||
)
|
||||
q = round(q)
|
||||
if q < 10:
|
||||
q = round(q, 2)
|
||||
|
@ -79,12 +80,15 @@ class QualityFactorChart(FrequencyChart):
|
|||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(3, y + 3, str(q))
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
|
||||
max_q = round(maxQ)
|
||||
|
@ -119,8 +123,9 @@ class QualityFactorChart(FrequencyChart):
|
|||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
Q = d.qFactor()
|
||||
return self.topMargin + int((self.maxQ - Q) / self.span *
|
||||
self.dim.height)
|
||||
return self.topMargin + int(
|
||||
(self.maxQ - Q) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
|
|
|
@ -62,11 +62,13 @@ class RealImaginaryChart(FrequencyChart):
|
|||
self.y_action_automatic.setCheckable(True)
|
||||
self.y_action_automatic.setChecked(True)
|
||||
self.y_action_automatic.changed.connect(
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||
)
|
||||
self.y_action_fixed_span = QtWidgets.QAction("Fixed span")
|
||||
self.y_action_fixed_span.setCheckable(True)
|
||||
self.y_action_fixed_span.changed.connect(
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedValues(self.y_action_fixed_span.isChecked())
|
||||
)
|
||||
mode_group = QtWidgets.QActionGroup(self)
|
||||
mode_group.addAction(self.y_action_automatic)
|
||||
mode_group.addAction(self.y_action_fixed_span)
|
||||
|
@ -110,11 +112,14 @@ class RealImaginaryChart(FrequencyChart):
|
|||
self.drawHorizontalTicks(qp)
|
||||
|
||||
fmt = Format(max_nr_digits=3)
|
||||
qp.drawText(3, self.dim.height + self.topMargin,
|
||||
str(Value(min_real, fmt=fmt)))
|
||||
qp.drawText(self.leftMargin + self.dim.width + 8,
|
||||
self.dim.height + self.topMargin,
|
||||
str(Value(min_imag, fmt=fmt)))
|
||||
qp.drawText(
|
||||
3, self.dim.height + self.topMargin, str(Value(min_real, fmt=fmt))
|
||||
)
|
||||
qp.drawText(
|
||||
self.leftMargin + self.dim.width + 8,
|
||||
self.dim.height + self.topMargin,
|
||||
str(Value(min_imag, fmt=fmt)),
|
||||
)
|
||||
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
||||
|
@ -131,8 +136,12 @@ class RealImaginaryChart(FrequencyChart):
|
|||
c.setAlpha(255)
|
||||
pen.setColor(c)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(self.leftMargin + self.dim.width, 9,
|
||||
self.leftMargin + self.dim.width + 5, 9)
|
||||
qp.drawLine(
|
||||
self.leftMargin + self.dim.width,
|
||||
9,
|
||||
self.leftMargin + self.dim.width + 5,
|
||||
9,
|
||||
)
|
||||
|
||||
primary_pen.setWidth(self.dim.point)
|
||||
secondary_pen.setWidth(self.dim.point)
|
||||
|
@ -161,7 +170,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_re, prev_x, prev_y_re)
|
||||
x, y_re, prev_x, prev_y_re
|
||||
)
|
||||
qp.drawLine(x, y_re, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_re):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||
|
@ -175,7 +185,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_im, prev_x, prev_y_im)
|
||||
x, y_im, prev_x, prev_y_im
|
||||
)
|
||||
qp.drawLine(x, y_im, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_im):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||
|
@ -197,8 +208,12 @@ class RealImaginaryChart(FrequencyChart):
|
|||
pen = QtGui.QPen(c)
|
||||
pen.setWidth(2)
|
||||
qp.setPen(pen)
|
||||
qp.drawLine(self.leftMargin + self.dim.width, 14,
|
||||
self.leftMargin + self.dim.width + 5, 14)
|
||||
qp.drawLine(
|
||||
self.leftMargin + self.dim.width,
|
||||
14,
|
||||
self.leftMargin + self.dim.width + 5,
|
||||
14,
|
||||
)
|
||||
|
||||
for i, reference in enumerate(self.reference):
|
||||
if reference.freq < self.fstart or reference.freq > self.fstop:
|
||||
|
@ -225,7 +240,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
qp.drawLine(x, y_re, prev_x, prev_y_re)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_re, prev_x, prev_y_re)
|
||||
x, y_re, prev_x, prev_y_re
|
||||
)
|
||||
qp.drawLine(x, y_re, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_re):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_re, x, y_re)
|
||||
|
@ -239,7 +255,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
qp.drawLine(x, y_im, prev_x, prev_y_im)
|
||||
else:
|
||||
new_x, new_y = self.getPlotable(
|
||||
x, y_im, prev_x, prev_y_im)
|
||||
x, y_im, prev_x, prev_y_im
|
||||
)
|
||||
qp.drawLine(x, y_im, new_x, new_y)
|
||||
elif self.isPlotable(prev_x, prev_y_im):
|
||||
new_x, new_y = self.getPlotable(prev_x, prev_y_im, x, y_im)
|
||||
|
@ -252,10 +269,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
y_re = self.getReYPosition(self.data[m.location])
|
||||
y_im = self.getImYPosition(self.data[m.location])
|
||||
|
||||
self.drawMarker(x, y_re, qp, m.color,
|
||||
self.markers.index(m) + 1)
|
||||
self.drawMarker(x, y_im, qp, m.color,
|
||||
self.markers.index(m) + 1)
|
||||
self.drawMarker(x, y_re, qp, m.color, self.markers.index(m) + 1)
|
||||
self.drawMarker(x, y_im, qp, m.color, self.markers.index(m) + 1)
|
||||
|
||||
def drawHorizontalTicks(self, qp):
|
||||
# We want one horizontal tick per 50 pixels, at most
|
||||
|
@ -264,8 +279,9 @@ class RealImaginaryChart(FrequencyChart):
|
|||
for i in range(horizontal_ticks):
|
||||
y = self.topMargin + i * self.dim.height // horizontal_ticks
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width + 5, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width + 5, y
|
||||
)
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
re = self.max_real - i * self.span_real / horizontal_ticks
|
||||
im = self.max_imag - i * self.span_imag / horizontal_ticks
|
||||
|
@ -273,7 +289,8 @@ class RealImaginaryChart(FrequencyChart):
|
|||
qp.drawText(
|
||||
self.leftMargin + self.dim.width + 8,
|
||||
y + 4,
|
||||
f"{Value(im, fmt=fmt)}")
|
||||
f"{Value(im, fmt=fmt)}",
|
||||
)
|
||||
|
||||
def find_scaling(self):
|
||||
# Find scaling
|
||||
|
@ -350,20 +367,24 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
def getImYPosition(self, d: Datapoint) -> int:
|
||||
im = self.value(d).imag
|
||||
return int(self.topMargin + (self.max_imag - im) / self.span_imag
|
||||
* self.dim.height)
|
||||
return int(
|
||||
self.topMargin
|
||||
+ (self.max_imag - im) / self.span_imag * self.dim.height
|
||||
)
|
||||
|
||||
def getReYPosition(self, d: Datapoint) -> int:
|
||||
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)
|
||||
return int(
|
||||
self.topMargin
|
||||
+ (self.max_real - re) / self.span_real * self.dim.height
|
||||
if math.isfinite(re)
|
||||
else self.topMargin
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
valRe = -1 * ((absy / self.dim.height *
|
||||
self.span_real) - self.max_real)
|
||||
valIm = -1 * ((absy / self.dim.height *
|
||||
self.span_imag) - self.max_imag)
|
||||
valRe = -1 * ((absy / self.dim.height * self.span_real) - self.max_real)
|
||||
valIm = -1 * ((absy / self.dim.height * self.span_imag) - self.max_imag)
|
||||
return [valRe, valIm]
|
||||
|
||||
def zoomTo(self, x1, y1, x2, y2):
|
||||
|
@ -406,9 +427,12 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
def setMinimumRealValue(self):
|
||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Minimum real value",
|
||||
"Set minimum real value", value=self.minDisplayReal,
|
||||
decimals=2)
|
||||
self,
|
||||
"Minimum real value",
|
||||
"Set minimum real value",
|
||||
value=self.minDisplayReal,
|
||||
decimals=2,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and min_val >= self.maxDisplayReal):
|
||||
|
@ -418,9 +442,12 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
def setMaximumRealValue(self):
|
||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Maximum real value",
|
||||
"Set maximum real value", value=self.maxDisplayReal,
|
||||
decimals=2)
|
||||
self,
|
||||
"Maximum real value",
|
||||
"Set maximum real value",
|
||||
value=self.maxDisplayReal,
|
||||
decimals=2,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and max_val <= self.minDisplayReal):
|
||||
|
@ -430,9 +457,12 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
def setMinimumImagValue(self):
|
||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Minimum imaginary value",
|
||||
"Set minimum imaginary value", value=self.minDisplayImag,
|
||||
decimals=2)
|
||||
self,
|
||||
"Minimum imaginary value",
|
||||
"Set minimum imaginary value",
|
||||
value=self.minDisplayImag,
|
||||
decimals=2,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and min_val >= self.maxDisplayImag):
|
||||
|
@ -442,9 +472,12 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
def setMaximumImagValue(self):
|
||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Maximum imaginary value",
|
||||
"Set maximum imaginary value", value=self.maxDisplayImag,
|
||||
decimals=2)
|
||||
self,
|
||||
"Maximum imaginary value",
|
||||
"Set maximum imaginary value",
|
||||
value=self.maxDisplayImag,
|
||||
decimals=2,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and max_val <= self.minDisplayImag):
|
||||
|
@ -454,9 +487,10 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
def setFixedValues(self, fixed_values: bool):
|
||||
self.fixedValues = fixed_values
|
||||
if (fixed_values and
|
||||
(self.minDisplayReal >= self.maxDisplayReal or
|
||||
self.minDisplayImag > self.maxDisplayImag)):
|
||||
if fixed_values and (
|
||||
self.minDisplayReal >= self.maxDisplayReal
|
||||
or self.minDisplayImag > self.maxDisplayImag
|
||||
):
|
||||
self.fixedValues = False
|
||||
self.y_action_automatic.setChecked(True)
|
||||
self.y_action_fixed_span.setChecked(False)
|
||||
|
@ -464,17 +498,23 @@ class RealImaginaryChart(FrequencyChart):
|
|||
|
||||
def contextMenuEvent(self, event):
|
||||
self.action_set_fixed_start.setText(
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})")
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_stop.setText(
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_minimum_real.setText(
|
||||
f"Minimum R ({self.minDisplayReal})")
|
||||
f"Minimum R ({self.minDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_maximum_real.setText(
|
||||
f"Maximum R ({self.maxDisplayReal})")
|
||||
f"Maximum R ({self.maxDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_minimum_imag.setText(
|
||||
f"Minimum jX ({self.minDisplayImag})")
|
||||
f"Minimum jX ({self.minDisplayImag})"
|
||||
)
|
||||
self.action_set_fixed_maximum_imag.setText(
|
||||
f"Maximum jX ({self.maxDisplayImag})")
|
||||
f"Maximum jX ({self.maxDisplayImag})"
|
||||
)
|
||||
self.menu.exec_(event.globalPos())
|
||||
|
||||
def value(self, p: Datapoint) -> complex:
|
||||
|
|
|
@ -34,30 +34,37 @@ 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})")
|
||||
f"Maximum {MU}' ({self.maxDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_maximum_real.triggered.connect(
|
||||
self.setMaximumRealValue)
|
||||
self.setMaximumRealValue
|
||||
)
|
||||
|
||||
self.action_set_fixed_minimum_real = QtWidgets.QAction(
|
||||
f"Minimum {MU}' ({self.minDisplayReal})")
|
||||
f"Minimum {MU}' ({self.minDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_minimum_real.triggered.connect(
|
||||
self.setMinimumRealValue)
|
||||
self.setMinimumRealValue
|
||||
)
|
||||
|
||||
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
|
||||
f"Maximum {MU}'' ({self.maxDisplayImag})")
|
||||
f"Maximum {MU}'' ({self.maxDisplayImag})"
|
||||
)
|
||||
self.action_set_fixed_maximum_imag.triggered.connect(
|
||||
self.setMaximumImagValue)
|
||||
self.setMaximumImagValue
|
||||
)
|
||||
|
||||
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
|
||||
f"Minimum {MU}'' ({self.minDisplayImag})")
|
||||
f"Minimum {MU}'' ({self.minDisplayImag})"
|
||||
)
|
||||
self.action_set_fixed_minimum_imag.triggered.connect(
|
||||
self.setMinimumImagValue)
|
||||
self.setMinimumImagValue
|
||||
)
|
||||
|
||||
self.y_menu.addAction(self.action_set_fixed_maximum_real)
|
||||
self.y_menu.addAction(self.action_set_fixed_minimum_real)
|
||||
|
@ -67,25 +74,21 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
|||
|
||||
# Manage core parameters
|
||||
# TODO pick some sane default values?
|
||||
self.coreLength = 1.
|
||||
self.coreArea = 1.
|
||||
self.coreLength = 1.0
|
||||
self.coreArea = 1.0
|
||||
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_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_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)
|
||||
"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)
|
||||
|
@ -102,41 +105,53 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
|||
|
||||
def drawChart(self, qp: QtGui.QPainter):
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(self.leftMargin + 5, 15,
|
||||
f"{self.name}")
|
||||
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)
|
||||
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)})")
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_stop.setText(
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_minimum_real.setText(
|
||||
f"Minimum {MU}' ({self.minDisplayReal})")
|
||||
f"Minimum {MU}' ({self.minDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_maximum_real.setText(
|
||||
f"Maximum {MU}' ({self.maxDisplayReal})")
|
||||
f"Maximum {MU}' ({self.maxDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_minimum_imag.setText(
|
||||
f"Minimum {MU}'' ({self.minDisplayImag})")
|
||||
f"Minimum {MU}'' ({self.minDisplayImag})"
|
||||
)
|
||||
self.action_set_fixed_maximum_imag.setText(
|
||||
f"Maximum {MU}'' ({self.maxDisplayImag})")
|
||||
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)
|
||||
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):
|
||||
|
@ -146,9 +161,12 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
|||
|
||||
def setCoreArea(self):
|
||||
val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Core effective area",
|
||||
self,
|
||||
"Core effective area",
|
||||
"Set core cross section area length in mm\N{SUPERSCRIPT TWO}",
|
||||
value=self.coreArea, decimals=2)
|
||||
value=self.coreArea,
|
||||
decimals=2,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and val >= 0):
|
||||
|
@ -158,8 +176,11 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
|||
|
||||
def setCoreWindings(self):
|
||||
val, selected = QtWidgets.QInputDialog.getInt(
|
||||
self, "Core number of windings",
|
||||
"Set core number of windings", value=self.coreWindings)
|
||||
self,
|
||||
"Core number of windings",
|
||||
"Set core number of windings",
|
||||
value=self.coreWindings,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and val >= 0):
|
||||
|
@ -176,6 +197,7 @@ class RealImaginaryMuChart(RealImaginaryChart):
|
|||
# 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))
|
||||
inductance
|
||||
* (self.coreLength / 1e3)
|
||||
/ (mu_0 * self.coreWindings**2 * (self.coreArea / 1e6))
|
||||
)
|
||||
|
|
|
@ -35,24 +35,32 @@ class RealImaginaryZChart(RealImaginaryChart):
|
|||
self.y_menu.addSeparator()
|
||||
|
||||
self.action_set_fixed_maximum_real = QtWidgets.QAction(
|
||||
f"Maximum R ({self.maxDisplayReal})")
|
||||
f"Maximum R ({self.maxDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_maximum_real.triggered.connect(
|
||||
self.setMaximumRealValue)
|
||||
self.setMaximumRealValue
|
||||
)
|
||||
|
||||
self.action_set_fixed_minimum_real = QtWidgets.QAction(
|
||||
f"Minimum R ({self.minDisplayReal})")
|
||||
f"Minimum R ({self.minDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_minimum_real.triggered.connect(
|
||||
self.setMinimumRealValue)
|
||||
self.setMinimumRealValue
|
||||
)
|
||||
|
||||
self.action_set_fixed_maximum_imag = QtWidgets.QAction(
|
||||
f"Maximum jX ({self.maxDisplayImag})")
|
||||
f"Maximum jX ({self.maxDisplayImag})"
|
||||
)
|
||||
self.action_set_fixed_maximum_imag.triggered.connect(
|
||||
self.setMaximumImagValue)
|
||||
self.setMaximumImagValue
|
||||
)
|
||||
|
||||
self.action_set_fixed_minimum_imag = QtWidgets.QAction(
|
||||
f"Minimum jX ({self.minDisplayImag})")
|
||||
f"Minimum jX ({self.minDisplayImag})"
|
||||
)
|
||||
self.action_set_fixed_minimum_imag.triggered.connect(
|
||||
self.setMinimumImagValue)
|
||||
self.setMinimumImagValue
|
||||
)
|
||||
|
||||
self.y_menu.addAction(self.action_set_fixed_maximum_real)
|
||||
self.y_menu.addAction(self.action_set_fixed_minimum_real)
|
||||
|
@ -62,34 +70,43 @@ class RealImaginaryZChart(RealImaginaryChart):
|
|||
|
||||
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(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)
|
||||
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)})")
|
||||
f"Start ({format_frequency_chart(self.minFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_stop.setText(
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})")
|
||||
f"Stop ({format_frequency_chart(self.maxFrequency)})"
|
||||
)
|
||||
self.action_set_fixed_minimum_real.setText(
|
||||
f"Minimum R ({self.minDisplayReal})")
|
||||
f"Minimum R ({self.minDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_maximum_real.setText(
|
||||
f"Maximum R ({self.maxDisplayReal})")
|
||||
f"Maximum R ({self.maxDisplayReal})"
|
||||
)
|
||||
self.action_set_fixed_minimum_imag.setText(
|
||||
f"Minimum jX ({self.minDisplayImag})")
|
||||
f"Minimum jX ({self.minDisplayImag})"
|
||||
)
|
||||
self.action_set_fixed_maximum_imag.setText(
|
||||
f"Maximum jX ({self.maxDisplayImag})")
|
||||
f"Maximum jX ({self.maxDisplayImag})"
|
||||
)
|
||||
self.menu.exec_(event.globalPos())
|
||||
|
||||
def value(self, p: Datapoint) -> complex:
|
||||
|
|
|
@ -25,6 +25,5 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class RealImaginaryZSeriesChart(RealImaginaryZChart):
|
||||
|
||||
def impedance(self, p: Datapoint) -> complex:
|
||||
return p.seriesImpedance()
|
||||
|
|
|
@ -25,6 +25,5 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class RealImaginaryZShuntChart(RealImaginaryZChart):
|
||||
|
||||
def impedance(self, p: Datapoint) -> complex:
|
||||
return p.shuntImpedance()
|
||||
|
|
|
@ -52,14 +52,18 @@ class SParameterChart(FrequencyChart):
|
|||
qp.drawText(10, 15, "Real")
|
||||
qp.drawText(self.leftMargin + self.dim.width - 15, 15, "Imag")
|
||||
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,
|
||||
self.topMargin + self.dim.height)
|
||||
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,
|
||||
self.topMargin + self.dim.height,
|
||||
)
|
||||
|
||||
def drawValues(self, qp: QtGui.QPainter):
|
||||
if len(self.data) == 0 and len(self.reference) == 0:
|
||||
|
@ -85,44 +89,58 @@ class SParameterChart(FrequencyChart):
|
|||
val = int(minValue + i * tick_step)
|
||||
y = self.topMargin + (maxValue - val) // span * self.dim.height
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
if val > minValue and val != maxValue:
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(3, y + 4, str(round(val, 2)))
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, self.topMargin,
|
||||
self.leftMargin + self.dim.width, self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(3, self.topMargin + 4, f"{maxValue}")
|
||||
qp.drawText(3, self.dim.height + self.topMargin, f"{minValue}")
|
||||
self.drawFrequencyTicks(qp)
|
||||
self.drawData(qp, self.data, Chart.color.sweep, self.getReYPosition)
|
||||
self.drawData(qp, self.reference, Chart.color.reference,
|
||||
self.getReYPosition)
|
||||
self.drawData(qp, self.data, Chart.color.sweep_secondary,
|
||||
self.getImYPosition)
|
||||
self.drawData(qp, self.reference,
|
||||
Chart.color.reference_secondary, self.getImYPosition)
|
||||
self.drawData(
|
||||
qp, self.reference, Chart.color.reference, self.getReYPosition
|
||||
)
|
||||
self.drawData(
|
||||
qp, self.data, Chart.color.sweep_secondary, self.getImYPosition
|
||||
)
|
||||
self.drawData(
|
||||
qp,
|
||||
self.reference,
|
||||
Chart.color.reference_secondary,
|
||||
self.getImYPosition,
|
||||
)
|
||||
|
||||
self.drawMarkers(qp, y_function=self.getReYPosition)
|
||||
self.drawMarkers(qp, y_function=self.getImYPosition)
|
||||
|
||||
def getYPosition(self, d: Datapoint) -> int:
|
||||
return int(
|
||||
self.topMargin + (self.maxValue - d.re) / self.span *
|
||||
self.dim.height)
|
||||
self.topMargin
|
||||
+ (self.maxValue - d.re) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def getReYPosition(self, d: Datapoint) -> int:
|
||||
return int(
|
||||
self.topMargin + (self.maxValue - d.re) / self.span *
|
||||
self.dim.height)
|
||||
self.topMargin
|
||||
+ (self.maxValue - d.re) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def getImYPosition(self, d: Datapoint) -> int:
|
||||
return int(
|
||||
self.topMargin + (self.maxValue - d.im) / self.span *
|
||||
self.dim.height)
|
||||
self.topMargin
|
||||
+ (self.maxValue - d.im) / self.span * self.dim.height
|
||||
)
|
||||
|
||||
def valueAtPosition(self, y) -> List[float]:
|
||||
absy = y - self.topMargin
|
||||
|
|
|
@ -35,58 +35,119 @@ class SmithChart(SquareChart):
|
|||
qp.drawText(3, 15, self.name)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawEllipse(QtCore.QPoint(center_x, center_y), width_2, height_2)
|
||||
qp.drawLine(center_x - width_2, center_y,
|
||||
center_x + width_2, center_y)
|
||||
qp.drawLine(center_x - width_2, center_y, center_x + width_2, center_y)
|
||||
|
||||
qp.drawEllipse(
|
||||
QtCore.QPoint(center_x + int(self.dim.width / 4), center_y),
|
||||
self.dim.width // 4, self.dim.height // 4) # Re(Z) = 1
|
||||
self.dim.width // 4,
|
||||
self.dim.height // 4,
|
||||
) # Re(Z) = 1
|
||||
qp.drawEllipse(
|
||||
QtCore.QPoint(center_x + self.dim.width // 3, center_y),
|
||||
self.dim.width // 6, self.dim.height // 6) # Re(Z) = 2
|
||||
self.dim.width // 6,
|
||||
self.dim.height // 6,
|
||||
) # Re(Z) = 2
|
||||
qp.drawEllipse(
|
||||
QtCore.QPoint(center_x + 3 * self.dim.width // 8, center_y),
|
||||
self.dim.width // 8, self.dim.height // 8) # Re(Z) = 3
|
||||
self.dim.width // 8,
|
||||
self.dim.height // 8,
|
||||
) # Re(Z) = 3
|
||||
qp.drawEllipse(
|
||||
QtCore.QPoint(center_x + 5 * self.dim.width // 12, center_y),
|
||||
self.dim.width // 12, self.dim.height // 12) # Re(Z) = 5
|
||||
self.dim.width // 12,
|
||||
self.dim.height // 12,
|
||||
) # Re(Z) = 5
|
||||
qp.drawEllipse(
|
||||
QtCore.QPoint(center_x + self.dim.width // 6, center_y),
|
||||
self.dim.width // 3, self.dim.height // 3) # Re(Z) = 0.5
|
||||
self.dim.width // 3,
|
||||
self.dim.height // 3,
|
||||
) # Re(Z) = 0.5
|
||||
qp.drawEllipse(
|
||||
QtCore.QPoint(center_x + self.dim.width // 12, center_y),
|
||||
5 * self.dim.width // 12, 5 * self.dim.height // 12) # Re(Z) = 0.2
|
||||
5 * self.dim.width // 12,
|
||||
5 * self.dim.height // 12,
|
||||
) # Re(Z) = 0.2
|
||||
|
||||
qp.drawArc(center_x + 3 * self.dim.width // 8, center_y,
|
||||
self.dim.width // 4, self.dim.width // 4,
|
||||
90 * 16, 152 * 16) # Im(Z) = -5
|
||||
qp.drawArc(center_x + 3 * self.dim.width // 8, center_y,
|
||||
self.dim.width // 4, -self.dim.width // 4,
|
||||
-90 * 16, -152 * 16) # Im(Z) = 5
|
||||
qp.drawArc(center_x + self.dim.width // 4, center_y,
|
||||
width_2, height_2,
|
||||
90 * 16, 127 * 16) # Im(Z) = -2
|
||||
qp.drawArc(center_x + self.dim.width // 4, center_y,
|
||||
width_2, -height_2,
|
||||
-90 * 16, -127 * 16) # Im(Z) = 2
|
||||
qp.drawArc(center_x, center_y,
|
||||
self.dim.width, self.dim.height,
|
||||
90 * 16, 90 * 16) # Im(Z) = -1
|
||||
qp.drawArc(center_x, center_y,
|
||||
self.dim.width, - self.dim.height,
|
||||
-90 * 16, -90 * 16) # Im(Z) = 1
|
||||
qp.drawArc(center_x - width_2, center_y,
|
||||
self.dim.width * 2, self.dim.height * 2,
|
||||
int(99.5 * 16), int(43.5 * 16)) # Im(Z) = -0.5
|
||||
qp.drawArc(center_x - width_2, center_y,
|
||||
self.dim.width * 2, -self.dim.height * 2,
|
||||
int(-99.5 * 16), int(-43.5 * 16)) # Im(Z) = 0.5
|
||||
qp.drawArc(center_x - self.dim.width * 2, center_y,
|
||||
self.dim.width * 5, self.dim.height * 5,
|
||||
int(93.85 * 16), int(18.85 * 16)) # Im(Z) = -0.2
|
||||
qp.drawArc(center_x - self.dim.width * 2, center_y,
|
||||
self.dim.width * 5, -self.dim.height * 5,
|
||||
int(-93.85 * 16), int(-18.85 * 16)) # Im(Z) = 0.2
|
||||
qp.drawArc(
|
||||
center_x + 3 * self.dim.width // 8,
|
||||
center_y,
|
||||
self.dim.width // 4,
|
||||
self.dim.width // 4,
|
||||
90 * 16,
|
||||
152 * 16,
|
||||
) # Im(Z) = -5
|
||||
qp.drawArc(
|
||||
center_x + 3 * self.dim.width // 8,
|
||||
center_y,
|
||||
self.dim.width // 4,
|
||||
-self.dim.width // 4,
|
||||
-90 * 16,
|
||||
-152 * 16,
|
||||
) # Im(Z) = 5
|
||||
qp.drawArc(
|
||||
center_x + self.dim.width // 4,
|
||||
center_y,
|
||||
width_2,
|
||||
height_2,
|
||||
90 * 16,
|
||||
127 * 16,
|
||||
) # Im(Z) = -2
|
||||
qp.drawArc(
|
||||
center_x + self.dim.width // 4,
|
||||
center_y,
|
||||
width_2,
|
||||
-height_2,
|
||||
-90 * 16,
|
||||
-127 * 16,
|
||||
) # Im(Z) = 2
|
||||
qp.drawArc(
|
||||
center_x,
|
||||
center_y,
|
||||
self.dim.width,
|
||||
self.dim.height,
|
||||
90 * 16,
|
||||
90 * 16,
|
||||
) # Im(Z) = -1
|
||||
qp.drawArc(
|
||||
center_x,
|
||||
center_y,
|
||||
self.dim.width,
|
||||
-self.dim.height,
|
||||
-90 * 16,
|
||||
-90 * 16,
|
||||
) # Im(Z) = 1
|
||||
qp.drawArc(
|
||||
center_x - width_2,
|
||||
center_y,
|
||||
self.dim.width * 2,
|
||||
self.dim.height * 2,
|
||||
int(99.5 * 16),
|
||||
int(43.5 * 16),
|
||||
) # Im(Z) = -0.5
|
||||
qp.drawArc(
|
||||
center_x - width_2,
|
||||
center_y,
|
||||
self.dim.width * 2,
|
||||
-self.dim.height * 2,
|
||||
int(-99.5 * 16),
|
||||
int(-43.5 * 16),
|
||||
) # Im(Z) = 0.5
|
||||
qp.drawArc(
|
||||
center_x - self.dim.width * 2,
|
||||
center_y,
|
||||
self.dim.width * 5,
|
||||
self.dim.height * 5,
|
||||
int(93.85 * 16),
|
||||
int(18.85 * 16),
|
||||
) # Im(Z) = -0.2
|
||||
qp.drawArc(
|
||||
center_x - self.dim.width * 2,
|
||||
center_y,
|
||||
self.dim.width * 5,
|
||||
-self.dim.height * 5,
|
||||
int(-93.85 * 16),
|
||||
int(-18.85 * 16),
|
||||
) # Im(Z) = 0.2
|
||||
|
||||
self.drawTitle(qp)
|
||||
|
||||
|
@ -99,4 +160,6 @@ class SmithChart(SquareChart):
|
|||
qp.drawEllipse(QtCore.QPoint(center_x, center_y), r, r)
|
||||
qp.drawText(
|
||||
QtCore.QRect(center_x - 50, center_y - 4 + r, 100, 20),
|
||||
QtCore.Qt.AlignCenter, f"{swr}")
|
||||
QtCore.Qt.AlignCenter,
|
||||
f"{swr}",
|
||||
)
|
||||
|
|
|
@ -29,11 +29,11 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SquareChart(Chart):
|
||||
def __init__(self, name=''):
|
||||
def __init__(self, name=""):
|
||||
super().__init__(name)
|
||||
sizepolicy = QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
QtWidgets.QSizePolicy.Fixed, QtWidgets.QSizePolicy.MinimumExpanding
|
||||
)
|
||||
self.setSizePolicy(sizepolicy)
|
||||
self.dim.width = 250
|
||||
self.dim.height = 250
|
||||
|
@ -53,8 +53,14 @@ class SquareChart(Chart):
|
|||
def drawChart(self, qp: QtGui.QPainter) -> None:
|
||||
raise NotImplementedError()
|
||||
|
||||
def draw_data(self, qp: QtGui.QPainter, color: QtGui.QColor,
|
||||
data: List[Datapoint], fstart: int = 0, fstop: int = 0):
|
||||
def draw_data(
|
||||
self,
|
||||
qp: QtGui.QPainter,
|
||||
color: QtGui.QColor,
|
||||
data: List[Datapoint],
|
||||
fstart: int = 0,
|
||||
fstop: int = 0,
|
||||
):
|
||||
if not data:
|
||||
return
|
||||
fstop = fstop or data[-1].freq
|
||||
|
@ -65,8 +71,7 @@ class SquareChart(Chart):
|
|||
|
||||
qp.setPen(pen)
|
||||
prev_x = self.getXPosition(data[0])
|
||||
prev_y = int(self.height() / 2 + data[0].im * -1 *
|
||||
self.dim.height / 2)
|
||||
prev_y = int(self.height() / 2 + data[0].im * -1 * self.dim.height / 2)
|
||||
for i, d in enumerate(data):
|
||||
x = self.getXPosition(d)
|
||||
y = int(self.height() / 2 + d.im * -1 * self.dim.height / 2)
|
||||
|
@ -85,14 +90,15 @@ class SquareChart(Chart):
|
|||
|
||||
fstart = self.data[0].freq if self.data else 0
|
||||
fstop = self.data[-1].freq if self.data else 0
|
||||
self.draw_data(qp, Chart.color.reference,
|
||||
self.reference, fstart, fstop)
|
||||
self.draw_data(qp, Chart.color.reference, self.reference, fstart, fstop)
|
||||
|
||||
for m in self.markers:
|
||||
if m.location != -1 and m.location < len(self.data):
|
||||
x = self.getXPosition(self.data[m.location])
|
||||
y = int(self.height() // 2 -
|
||||
self.data[m.location].im * self.dim.height // 2)
|
||||
y = int(
|
||||
self.height() // 2
|
||||
- self.data[m.location].im * self.dim.height // 2
|
||||
)
|
||||
self.drawMarker(x, y, qp, m.color, self.markers.index(m) + 1)
|
||||
|
||||
def resizeEvent(self, a0: QtGui.QResizeEvent) -> None:
|
||||
|
@ -114,11 +120,13 @@ class SquareChart(Chart):
|
|||
y = a0.y()
|
||||
absx = x - (self.width() - self.dim.width) / 2
|
||||
absy = y - (self.height() - self.dim.height) / 2
|
||||
if (absx < 0 or
|
||||
absx > self.dim.width or
|
||||
absy < 0 or
|
||||
absy > self.dim.height or
|
||||
(not self.data and not self.reference)):
|
||||
if (
|
||||
absx < 0
|
||||
or absx > self.dim.width
|
||||
or absy < 0
|
||||
or absy > self.dim.height
|
||||
or (not self.data and not self.reference)
|
||||
):
|
||||
a0.ignore()
|
||||
return
|
||||
a0.accept()
|
||||
|
@ -133,8 +141,9 @@ class SquareChart(Chart):
|
|||
|
||||
positions = [
|
||||
math.sqrt(
|
||||
(x - (width_2 + d.re * dim_x_2))**2 +
|
||||
(y - (height_2 - d.im * dim_y_2))**2)
|
||||
(x - (width_2 + d.re * dim_x_2)) ** 2
|
||||
+ (y - (height_2 - d.im * dim_y_2)) ** 2
|
||||
)
|
||||
for d in target
|
||||
]
|
||||
|
||||
|
|
|
@ -49,7 +49,9 @@ class TDRChart(Chart):
|
|||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding))
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
)
|
||||
)
|
||||
pal = QtGui.QPalette()
|
||||
pal.setColor(QtGui.QPalette.Background, Chart.color.background)
|
||||
self.setPalette(pal)
|
||||
|
@ -68,11 +70,13 @@ class TDRChart(Chart):
|
|||
self.action_automatic.setCheckable(True)
|
||||
self.action_automatic.setChecked(True)
|
||||
self.action_automatic.changed.connect(
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||
)
|
||||
self.action_fixed_span = QtWidgets.QAction("Fixed span")
|
||||
self.action_fixed_span.setCheckable(True)
|
||||
self.action_fixed_span.changed.connect(
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked()))
|
||||
lambda: self.setFixedSpan(self.action_fixed_span.isChecked())
|
||||
)
|
||||
self.mode_group.addAction(self.action_automatic)
|
||||
self.mode_group.addAction(self.action_fixed_span)
|
||||
self.x_menu.addAction(self.action_automatic)
|
||||
|
@ -80,11 +84,13 @@ class TDRChart(Chart):
|
|||
self.x_menu.addSeparator()
|
||||
|
||||
self.action_set_fixed_start = QtWidgets.QAction(
|
||||
f"Start ({self.minDisplayLength})")
|
||||
f"Start ({self.minDisplayLength})"
|
||||
)
|
||||
self.action_set_fixed_start.triggered.connect(self.setMinimumLength)
|
||||
|
||||
self.action_set_fixed_stop = QtWidgets.QAction(
|
||||
f"Stop ({self.maxDisplayLength})")
|
||||
f"Stop ({self.maxDisplayLength})"
|
||||
)
|
||||
self.action_set_fixed_stop.triggered.connect(self.setMaximumLength)
|
||||
|
||||
self.x_menu.addAction(self.action_set_fixed_start)
|
||||
|
@ -96,11 +102,13 @@ class TDRChart(Chart):
|
|||
self.y_action_automatic.setCheckable(True)
|
||||
self.y_action_automatic.setChecked(True)
|
||||
self.y_action_automatic.changed.connect(
|
||||
lambda: self.setFixedValues(self.y_action_fixed.isChecked()))
|
||||
lambda: self.setFixedValues(self.y_action_fixed.isChecked())
|
||||
)
|
||||
self.y_action_fixed = QtWidgets.QAction("Fixed")
|
||||
self.y_action_fixed.setCheckable(True)
|
||||
self.y_action_fixed.changed.connect(
|
||||
lambda: self.setFixedValues(self.y_action_fixed.isChecked()))
|
||||
lambda: self.setFixedValues(self.y_action_fixed.isChecked())
|
||||
)
|
||||
self.y_mode_group.addAction(self.y_action_automatic)
|
||||
self.y_mode_group.addAction(self.y_action_fixed)
|
||||
self.y_menu.addAction(self.y_action_automatic)
|
||||
|
@ -108,14 +116,18 @@ class TDRChart(Chart):
|
|||
self.y_menu.addSeparator()
|
||||
|
||||
self.y_action_set_fixed_maximum = QtWidgets.QAction(
|
||||
f"Maximum ({self.maxImpedance})")
|
||||
f"Maximum ({self.maxImpedance})"
|
||||
)
|
||||
self.y_action_set_fixed_maximum.triggered.connect(
|
||||
self.setMaximumImpedance)
|
||||
self.setMaximumImpedance
|
||||
)
|
||||
|
||||
self.y_action_set_fixed_minimum = QtWidgets.QAction(
|
||||
f"Minimum ({self.minImpedance})")
|
||||
f"Minimum ({self.minImpedance})"
|
||||
)
|
||||
self.y_action_set_fixed_minimum.triggered.connect(
|
||||
self.setMinimumImpedance)
|
||||
self.setMinimumImpedance
|
||||
)
|
||||
|
||||
self.y_menu.addAction(self.y_action_set_fixed_maximum)
|
||||
self.y_menu.addAction(self.y_action_set_fixed_minimum)
|
||||
|
@ -126,26 +138,29 @@ class TDRChart(Chart):
|
|||
self.menu.addAction(self.action_save_screenshot)
|
||||
self.action_popout = QtWidgets.QAction("Popout chart")
|
||||
self.action_popout.triggered.connect(
|
||||
lambda: self.popoutRequested.emit(self))
|
||||
lambda: self.popoutRequested.emit(self)
|
||||
)
|
||||
self.menu.addAction(self.action_popout)
|
||||
|
||||
self.dim.width = self.width() - self.leftMargin - self.rightMargin
|
||||
self.dim.height = self.height() - self.bottomMargin - self.topMargin
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
self.action_set_fixed_start.setText(
|
||||
f"Start ({self.minDisplayLength})")
|
||||
self.action_set_fixed_stop.setText(
|
||||
f"Stop ({self.maxDisplayLength})")
|
||||
self.action_set_fixed_start.setText(f"Start ({self.minDisplayLength})")
|
||||
self.action_set_fixed_stop.setText(f"Stop ({self.maxDisplayLength})")
|
||||
self.y_action_set_fixed_minimum.setText(
|
||||
f"Minimum ({self.minImpedance})")
|
||||
f"Minimum ({self.minImpedance})"
|
||||
)
|
||||
self.y_action_set_fixed_maximum.setText(
|
||||
f"Maximum ({self.maxImpedance})")
|
||||
f"Maximum ({self.maxImpedance})"
|
||||
)
|
||||
self.menu.exec_(event.globalPos())
|
||||
|
||||
def isPlotable(self, x, y):
|
||||
return self.leftMargin <= x <= self.width() - self.rightMargin and \
|
||||
self.topMargin <= y <= self.height() - self.bottomMargin
|
||||
return (
|
||||
self.leftMargin <= x <= self.width() - self.rightMargin
|
||||
and self.topMargin <= y <= self.height() - self.bottomMargin
|
||||
)
|
||||
|
||||
def resetDisplayLimits(self):
|
||||
self.fixedSpan = False
|
||||
|
@ -162,9 +177,13 @@ class TDRChart(Chart):
|
|||
|
||||
def setMinimumLength(self):
|
||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Start length (m)",
|
||||
"Set start length (m)", value=self.minDisplayLength,
|
||||
min=0, decimals=1)
|
||||
self,
|
||||
"Start length (m)",
|
||||
"Set start length (m)",
|
||||
value=self.minDisplayLength,
|
||||
min=0,
|
||||
decimals=1,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedSpan and min_val >= self.maxDisplayLength):
|
||||
|
@ -174,9 +193,13 @@ class TDRChart(Chart):
|
|||
|
||||
def setMaximumLength(self):
|
||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Stop length (m)",
|
||||
"Set stop length (m)", value=self.minDisplayLength,
|
||||
min=0.1, decimals=1)
|
||||
self,
|
||||
"Stop length (m)",
|
||||
"Set stop length (m)",
|
||||
value=self.minDisplayLength,
|
||||
min=0.1,
|
||||
decimals=1,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedSpan and max_val <= self.minDisplayLength):
|
||||
|
@ -190,10 +213,13 @@ class TDRChart(Chart):
|
|||
|
||||
def setMinimumImpedance(self):
|
||||
min_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Minimum impedance (\N{OHM SIGN})",
|
||||
self,
|
||||
"Minimum impedance (\N{OHM SIGN})",
|
||||
"Set minimum impedance (\N{OHM SIGN})",
|
||||
value=self.minDisplayLength,
|
||||
min=0, decimals=1)
|
||||
min=0,
|
||||
decimals=1,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and min_val >= self.maxImpedance):
|
||||
|
@ -203,10 +229,13 @@ class TDRChart(Chart):
|
|||
|
||||
def setMaximumImpedance(self):
|
||||
max_val, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Maximum impedance (\N{OHM SIGN})",
|
||||
self,
|
||||
"Maximum impedance (\N{OHM SIGN})",
|
||||
"Set maximum impedance (\N{OHM SIGN})",
|
||||
value=self.minDisplayLength,
|
||||
min=0.1, decimals=1)
|
||||
min=0.1,
|
||||
decimals=1,
|
||||
)
|
||||
if not selected:
|
||||
return
|
||||
if not (self.fixedValues and max_val <= self.minImpedance):
|
||||
|
@ -236,9 +265,12 @@ class TDRChart(Chart):
|
|||
if self.dragbox.move_x != -1 and self.dragbox.move_y != -1:
|
||||
dx = self.dragbox.move_x - a0.x()
|
||||
dy = self.dragbox.move_y - a0.y()
|
||||
self.zoomTo(self.leftMargin + dx, self.topMargin + dy,
|
||||
self.leftMargin + self.dim.width + dx,
|
||||
self.topMargin + self.dim.height + dy)
|
||||
self.zoomTo(
|
||||
self.leftMargin + dx,
|
||||
self.topMargin + dy,
|
||||
self.leftMargin + self.dim.width + dx,
|
||||
self.topMargin + self.dim.height + dy,
|
||||
)
|
||||
self.dragbox.move_x = a0.x()
|
||||
self.dragbox.move_y = a0.y()
|
||||
return
|
||||
|
@ -261,13 +293,14 @@ class TDRChart(Chart):
|
|||
if self.tdrWindow.td:
|
||||
if self.fixedSpan:
|
||||
max_index = np.searchsorted(
|
||||
self.tdrWindow.distance_axis, self.maxDisplayLength * 2)
|
||||
self.tdrWindow.distance_axis, self.maxDisplayLength * 2
|
||||
)
|
||||
min_index = np.searchsorted(
|
||||
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
|
||||
self.tdrWindow.distance_axis, self.minDisplayLength * 2
|
||||
)
|
||||
x_step = (max_index - min_index) / width
|
||||
else:
|
||||
max_index = math.ceil(
|
||||
len(self.tdrWindow.distance_axis) / 2)
|
||||
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||
x_step = max_index / width
|
||||
|
||||
self.markerLocation = int(round(absx * x_step))
|
||||
|
@ -282,17 +315,21 @@ class TDRChart(Chart):
|
|||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(x, self.topMargin, x, self.topMargin + height)
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
distance = self.tdrWindow.distance_axis[
|
||||
min_index +
|
||||
int((x - self.leftMargin) * x_step) - 1] / 2
|
||||
qp.drawText(x - 15, self.topMargin + height + 15,
|
||||
f"{round(distance, 1)}m")
|
||||
distance = (
|
||||
self.tdrWindow.distance_axis[
|
||||
min_index + int((x - self.leftMargin) * x_step) - 1
|
||||
]
|
||||
/ 2
|
||||
)
|
||||
qp.drawText(
|
||||
x - 15, self.topMargin + height + 15, f"{round(distance, 1)}m"
|
||||
)
|
||||
qp.setPen(QtGui.QPen(Chart.color.text))
|
||||
qp.drawText(
|
||||
self.leftMargin - 10,
|
||||
self.topMargin + height + 15,
|
||||
str(round(self.tdrWindow.distance_axis[min_index] / 2,
|
||||
1)) + "m")
|
||||
str(round(self.tdrWindow.distance_axis[min_index] / 2, 1)) + "m",
|
||||
)
|
||||
|
||||
def _draw_y_ticks(self, height, width, min_impedance, max_impedance):
|
||||
qp = QtGui.QPainter(self)
|
||||
|
@ -308,7 +345,8 @@ class TDRChart(Chart):
|
|||
qp.drawText(3, y + 3, str(round(y_val, 1)))
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(
|
||||
3, self.topMargin + height + 3, f"{round(min_impedance, 1)}")
|
||||
3, self.topMargin + height + 3, f"{round(min_impedance, 1)}"
|
||||
)
|
||||
|
||||
def _draw_max_point(self, height, x_step, y_step, min_index):
|
||||
qp = QtGui.QPainter(self)
|
||||
|
@ -316,22 +354,25 @@ class TDRChart(Chart):
|
|||
|
||||
max_point = QtCore.QPoint(
|
||||
self.leftMargin + int((id_max - min_index) / x_step),
|
||||
(self.topMargin + height) - int(
|
||||
self.tdrWindow.td[id_max] / y_step))
|
||||
(self.topMargin + height) - int(self.tdrWindow.td[id_max] / y_step),
|
||||
)
|
||||
|
||||
qp.setPen(self.markers[0].color)
|
||||
qp.drawEllipse(max_point, 2, 2)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawText(max_point.x() - 10, max_point.y() - 5,
|
||||
f"{round(self.tdrWindow.distance_axis[id_max] / 2, 2)}m")
|
||||
qp.drawText(
|
||||
max_point.x() - 10,
|
||||
max_point.y() - 5,
|
||||
f"{round(self.tdrWindow.distance_axis[id_max] / 2, 2)}m",
|
||||
)
|
||||
|
||||
def _draw_marker(self, height, x_step, y_step, min_index):
|
||||
qp = QtGui.QPainter(self)
|
||||
marker_point = QtCore.QPoint(
|
||||
self.leftMargin +
|
||||
int((self.markerLocation - min_index) / x_step),
|
||||
(self.topMargin + height) -
|
||||
int(self.tdrWindow.td[self.markerLocation] / y_step))
|
||||
self.leftMargin + int((self.markerLocation - min_index) / x_step),
|
||||
(self.topMargin + height)
|
||||
- int(self.tdrWindow.td[self.markerLocation] / y_step),
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
qp.drawEllipse(marker_point, 2, 2)
|
||||
qp.drawText(
|
||||
|
@ -339,19 +380,21 @@ class TDRChart(Chart):
|
|||
marker_point.y() - 5,
|
||||
f"""{round(
|
||||
self.tdrWindow.distance_axis[self.markerLocation] / 2,
|
||||
2)}m""")
|
||||
2)}m""",
|
||||
)
|
||||
|
||||
def _draw_graph(self, height, width):
|
||||
min_index = 0
|
||||
max_index = math.ceil(
|
||||
len(self.tdrWindow.distance_axis) / 2)
|
||||
max_index = math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||
|
||||
if self.fixedSpan:
|
||||
max_length = max(0.1, self.maxDisplayLength)
|
||||
max_index = np.searchsorted(
|
||||
self.tdrWindow.distance_axis, max_length * 2)
|
||||
self.tdrWindow.distance_axis, max_length * 2
|
||||
)
|
||||
min_index = np.searchsorted(
|
||||
self.tdrWindow.distance_axis, self.minDisplayLength * 2)
|
||||
self.tdrWindow.distance_axis, self.minDisplayLength * 2
|
||||
)
|
||||
if max_index == min_index:
|
||||
if max_index < len(self.tdrWindow.distance_axis) - 1:
|
||||
max_index += 1
|
||||
|
@ -361,8 +404,7 @@ class TDRChart(Chart):
|
|||
|
||||
# TODO: Limit the search to the selected span?
|
||||
min_impedance = max(0, np.min(self.tdrWindow.step_response_Z) / 1.05)
|
||||
max_impedance = min(1000, np.max(
|
||||
self.tdrWindow.step_response_Z) * 1.05)
|
||||
max_impedance = min(1000, np.max(self.tdrWindow.step_response_Z) * 1.05)
|
||||
if self.fixedValues:
|
||||
min_impedance = max(0, self.minImpedance)
|
||||
max_impedance = max(0.1, self.maxImpedance)
|
||||
|
@ -370,7 +412,7 @@ class TDRChart(Chart):
|
|||
y_step = max(self.tdrWindow.td) * 1.1 / height or 1.0e-30
|
||||
|
||||
self._draw_ticks(height, width, x_step, min_index)
|
||||
self._draw_y_ticks(height, width, min_impedance, max_impedance)
|
||||
self._draw_y_ticks(height, width, min_impedance, max_impedance)
|
||||
|
||||
qp = QtGui.QPainter(self)
|
||||
pen = QtGui.QPen(Chart.color.sweep)
|
||||
|
@ -388,7 +430,8 @@ class TDRChart(Chart):
|
|||
|
||||
x = self.leftMargin + int((i - min_index) / x_step)
|
||||
y = (self.topMargin + height) - int(
|
||||
(self.tdrWindow.step_response_Z[i] - min_impedance) / y_step)
|
||||
(self.tdrWindow.step_response_Z[i] - min_impedance) / y_step
|
||||
)
|
||||
if self.isPlotable(x, y):
|
||||
pen.setColor(Chart.color.sweep_secondary)
|
||||
qp.setPen(pen)
|
||||
|
@ -408,14 +451,18 @@ class TDRChart(Chart):
|
|||
height = self.height() - self.bottomMargin - self.topMargin
|
||||
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5,
|
||||
self.height() - self.bottomMargin,
|
||||
self.width() - self.rightMargin,
|
||||
self.height() - self.bottomMargin)
|
||||
qp.drawLine(self.leftMargin,
|
||||
self.topMargin - 5,
|
||||
self.leftMargin,
|
||||
self.height() - self.bottomMargin + 5)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.height() - self.bottomMargin,
|
||||
self.width() - self.rightMargin,
|
||||
self.height() - self.bottomMargin,
|
||||
)
|
||||
qp.drawLine(
|
||||
self.leftMargin,
|
||||
self.topMargin - 5,
|
||||
self.leftMargin,
|
||||
self.height() - self.bottomMargin + 5,
|
||||
)
|
||||
# Number of ticks does not include the origin
|
||||
self.drawTitle(qp)
|
||||
|
||||
|
@ -424,12 +471,13 @@ class TDRChart(Chart):
|
|||
|
||||
if self.dragbox.state and self.dragbox.pos[0] != -1:
|
||||
dashed_pen = QtGui.QPen(
|
||||
Chart.color.foreground, 1, QtCore.Qt.DashLine)
|
||||
Chart.color.foreground, 1, QtCore.Qt.DashLine
|
||||
)
|
||||
qp.setPen(dashed_pen)
|
||||
qp.drawRect(
|
||||
QtCore.QRect(
|
||||
QtCore.QPoint(*self.dragbox.pos_start),
|
||||
QtCore.QPoint(*self.dragbox.pos)
|
||||
QtCore.QPoint(*self.dragbox.pos),
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -444,11 +492,11 @@ class TDRChart(Chart):
|
|||
max_impedance = self.maxImpedance
|
||||
else:
|
||||
min_impedance = max(
|
||||
0,
|
||||
np.min(self.tdrWindow.step_response_Z) / 1.05)
|
||||
0, np.min(self.tdrWindow.step_response_Z) / 1.05
|
||||
)
|
||||
max_impedance = min(
|
||||
1000,
|
||||
np.max(self.tdrWindow.step_response_Z) * 1.05)
|
||||
1000, np.max(self.tdrWindow.step_response_Z) * 1.05
|
||||
)
|
||||
y_step = (max_impedance - min_impedance) / height
|
||||
return y_step * absy + min_impedance
|
||||
return 0
|
||||
|
@ -459,20 +507,28 @@ class TDRChart(Chart):
|
|||
width = self.width() - self.leftMargin - self.rightMargin
|
||||
absx = x - self.leftMargin
|
||||
min_length = self.minDisplayLength if self.fixedSpan else 0
|
||||
max_length = self.maxDisplayLength if self.fixedSpan else (
|
||||
self.tdrWindow.distance_axis[
|
||||
math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||
] / 2)
|
||||
max_length = (
|
||||
self.maxDisplayLength
|
||||
if self.fixedSpan
|
||||
else (
|
||||
self.tdrWindow.distance_axis[
|
||||
math.ceil(len(self.tdrWindow.distance_axis) / 2)
|
||||
]
|
||||
/ 2
|
||||
)
|
||||
)
|
||||
|
||||
x_step = (max_length - min_length) / width
|
||||
if limit and absx < 0:
|
||||
return min_length
|
||||
return (max_length if limit and absx > width else
|
||||
absx * x_step + min_length)
|
||||
return (
|
||||
max_length if limit and absx > width else absx * x_step + min_length
|
||||
)
|
||||
|
||||
def zoomTo(self, x1, y1, x2, y2):
|
||||
logger.debug(
|
||||
"Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2)
|
||||
"Zoom to (x,y) by (x,y): (%d, %d) by (%d, %d)", x1, y1, x2, y2
|
||||
)
|
||||
val1 = self.valueAtPosition(y1)
|
||||
val2 = self.valueAtPosition(y2)
|
||||
|
||||
|
|
|
@ -30,7 +30,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class VSWRChart(FrequencyChart):
|
||||
|
||||
def __init__(self, name=""):
|
||||
super().__init__(name)
|
||||
|
||||
|
@ -90,19 +89,22 @@ class VSWRChart(FrequencyChart):
|
|||
qp.setPen(Chart.color.text)
|
||||
if vswr != 0:
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr)))))
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr))))
|
||||
)
|
||||
v_text = f"{round(vswr, digits)}" if digits else "0"
|
||||
qp.drawText(3, y + 3, v_text)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(self.leftMargin - 5,
|
||||
self.topMargin + self.dim.height,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin + self.dim.height,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin + self.dim.height,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(minVSWR)))))
|
||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(minVSWR)))))
|
||||
v_text = f"{round(minVSWR, digits)}" if digits else "0"
|
||||
qp.drawText(3, self.topMargin + self.dim.height, v_text)
|
||||
else:
|
||||
|
@ -112,16 +114,20 @@ class VSWRChart(FrequencyChart):
|
|||
qp.setPen(Chart.color.text)
|
||||
if vswr != 0:
|
||||
digits = max(
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr)))))
|
||||
0, min(2, math.floor(3 - math.log10(abs(vswr))))
|
||||
)
|
||||
vswrstr = f"{round(vswr, digits)}" if digits else "0"
|
||||
qp.drawText(3, y + 3, vswrstr)
|
||||
qp.setPen(QtGui.QPen(Chart.color.foreground))
|
||||
qp.drawLine(self.leftMargin - 5, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5, y, self.leftMargin + self.dim.width, y
|
||||
)
|
||||
qp.drawLine(
|
||||
self.leftMargin - 5,
|
||||
self.topMargin,
|
||||
self.leftMargin + self.dim.width,
|
||||
self.topMargin,
|
||||
)
|
||||
qp.setPen(Chart.color.text)
|
||||
digits = max(0, min(2, math.floor(3 - math.log10(abs(maxVSWR)))))
|
||||
v_text = f"{round(maxVSWR, digits)}" if digits else "0"
|
||||
|
@ -130,8 +136,7 @@ class VSWRChart(FrequencyChart):
|
|||
qp.setPen(Chart.color.swr)
|
||||
for vswr in self.swrMarkers:
|
||||
y = self.getYPositionFromValue(vswr)
|
||||
qp.drawLine(self.leftMargin, y,
|
||||
self.leftMargin + self.dim.width, y)
|
||||
qp.drawLine(self.leftMargin, y, self.leftMargin + self.dim.width, y)
|
||||
qp.drawText(self.leftMargin + 3, y - 1, str(vswr))
|
||||
|
||||
self.drawFrequencyTicks(qp)
|
||||
|
@ -146,13 +151,15 @@ class VSWRChart(FrequencyChart):
|
|||
span = math.log(self.maxVSWR) - math.log(min_val)
|
||||
else:
|
||||
return -1
|
||||
return (
|
||||
self.topMargin + int(
|
||||
(math.log(self.maxVSWR) - math.log(vswr)) /
|
||||
span * self.dim.height))
|
||||
return self.topMargin + int(
|
||||
(math.log(self.maxVSWR) - math.log(vswr))
|
||||
/ span
|
||||
* self.dim.height
|
||||
)
|
||||
try:
|
||||
return self.topMargin + int(
|
||||
(self.maxVSWR - vswr) / self.span * self.dim.height)
|
||||
(self.maxVSWR - vswr) / self.span * self.dim.height
|
||||
)
|
||||
except OverflowError:
|
||||
return self.topMargin
|
||||
|
||||
|
|
|
@ -23,30 +23,31 @@ from .Smith import SmithChart
|
|||
from .SParam import SParameterChart
|
||||
from .TDR import TDRChart
|
||||
from .VSWR import VSWRChart
|
||||
|
||||
__all__ = [
|
||||
'Chart',
|
||||
'FrequencyChart',
|
||||
'PolarChart',
|
||||
'SquareChart',
|
||||
'CapacitanceChart',
|
||||
'InductanceChart',
|
||||
'GroupDelayChart',
|
||||
'LogMagChart',
|
||||
'CombinedLogMagChart',
|
||||
'MagnitudeChart',
|
||||
'MagnitudeZChart',
|
||||
'MagnitudeZShuntChart',
|
||||
'MagnitudeZSeriesChart',
|
||||
'PermeabilityChart',
|
||||
'PhaseChart',
|
||||
'QualityFactorChart',
|
||||
'RealImaginaryChart',
|
||||
'RealImaginaryMuChart',
|
||||
'RealImaginaryZChart',
|
||||
'RealImaginaryZShuntChart',
|
||||
'RealImaginaryZSeriesChart',
|
||||
'SmithChart',
|
||||
'SParameterChart',
|
||||
'TDRChart',
|
||||
'VSWRChart',
|
||||
"Chart",
|
||||
"FrequencyChart",
|
||||
"PolarChart",
|
||||
"SquareChart",
|
||||
"CapacitanceChart",
|
||||
"InductanceChart",
|
||||
"GroupDelayChart",
|
||||
"LogMagChart",
|
||||
"CombinedLogMagChart",
|
||||
"MagnitudeChart",
|
||||
"MagnitudeZChart",
|
||||
"MagnitudeZShuntChart",
|
||||
"MagnitudeZSeriesChart",
|
||||
"PermeabilityChart",
|
||||
"PhaseChart",
|
||||
"QualityFactorChart",
|
||||
"RealImaginaryChart",
|
||||
"RealImaginaryMuChart",
|
||||
"RealImaginaryZChart",
|
||||
"RealImaginaryZShuntChart",
|
||||
"RealImaginaryZSeriesChart",
|
||||
"SmithChart",
|
||||
"SParameterChart",
|
||||
"TDRChart",
|
||||
"VSWRChart",
|
||||
]
|
||||
|
|
|
@ -29,16 +29,16 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class ShowButton(QtWidgets.QPushButton):
|
||||
def setText(self, text: str = ''):
|
||||
def setText(self, text: str = ""):
|
||||
if not text:
|
||||
text = ("Show data"
|
||||
if Defaults.cfg.gui.markers_hidden else "Hide data")
|
||||
text = (
|
||||
"Show data" if Defaults.cfg.gui.markers_hidden else "Hide data"
|
||||
)
|
||||
super().setText(text)
|
||||
self.setToolTip("Toggle visibility of marker readings area")
|
||||
|
||||
|
||||
class MarkerControl(Control):
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__(app, "Markers")
|
||||
|
||||
|
@ -72,7 +72,8 @@ class MarkerControl(Control):
|
|||
lock_radiobutton = QtWidgets.QRadioButton("Locked")
|
||||
lock_radiobutton.setLayoutDirection(QtCore.Qt.RightToLeft)
|
||||
lock_radiobutton.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred)
|
||||
QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Preferred
|
||||
)
|
||||
|
||||
hbox = QtWidgets.QHBoxLayout()
|
||||
hbox.addWidget(self.showMarkerButton)
|
||||
|
@ -82,8 +83,7 @@ class MarkerControl(Control):
|
|||
def toggle_frame(self):
|
||||
def settings(hidden: bool):
|
||||
Defaults.cfg.gui.markers_hidden = not hidden
|
||||
self.app.marker_frame.setHidden(
|
||||
Defaults.cfg.gui.markers_hidden)
|
||||
self.app.marker_frame.setHidden(Defaults.cfg.gui.markers_hidden)
|
||||
self.showMarkerButton.setText()
|
||||
self.showMarkerButton.repaint()
|
||||
|
||||
|
|
|
@ -28,7 +28,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SerialControl(Control):
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__(app, "Serial port control")
|
||||
|
||||
|
@ -58,7 +57,8 @@ class SerialControl(Control):
|
|||
self.btn_settings.setMinimumHeight(20)
|
||||
self.btn_settings.setFixedWidth(60)
|
||||
self.btn_settings.clicked.connect(
|
||||
lambda: self.app.display_window("device_settings"))
|
||||
lambda: self.app.display_window("device_settings")
|
||||
)
|
||||
|
||||
button_layout.addWidget(self.btn_settings, stretch=0)
|
||||
self.layout.addRow(button_layout)
|
||||
|
@ -82,8 +82,9 @@ class SerialControl(Control):
|
|||
try:
|
||||
self.interface.open()
|
||||
except (IOError, AttributeError) as exc:
|
||||
logger.error("Tried to open %s and failed: %s",
|
||||
self.interface, exc)
|
||||
logger.error(
|
||||
"Tried to open %s and failed: %s", self.interface, exc
|
||||
)
|
||||
return
|
||||
if not self.interface.isOpen():
|
||||
logger.error("Unable to open port %s", self.interface)
|
||||
|
@ -96,7 +97,8 @@ class SerialControl(Control):
|
|||
logger.error("Unable to connect to VNA: %s", exc)
|
||||
|
||||
self.app.vna.validateInput = self.app.settings.value(
|
||||
"SerialInputValidation", True, bool)
|
||||
"SerialInputValidation", True, bool
|
||||
)
|
||||
|
||||
# connected
|
||||
self.btn_toggle.setText("Disconnect")
|
||||
|
@ -106,16 +108,20 @@ class SerialControl(Control):
|
|||
if not frequencies:
|
||||
logger.warning("No frequencies read")
|
||||
return
|
||||
logger.info("Read starting frequency %s and end frequency %s",
|
||||
frequencies[0], frequencies[-1])
|
||||
logger.info(
|
||||
"Read starting frequency %s and end frequency %s",
|
||||
frequencies[0],
|
||||
frequencies[-1],
|
||||
)
|
||||
self.app.sweep_control.set_start(frequencies[0])
|
||||
if frequencies[0] < frequencies[-1]:
|
||||
self.app.sweep_control.set_end(frequencies[-1])
|
||||
else:
|
||||
self.app.sweep_control.set_end(
|
||||
frequencies[0] +
|
||||
self.app.vna.datapoints *
|
||||
self.app.sweep_control.get_segments())
|
||||
frequencies[0]
|
||||
+ self.app.vna.datapoints
|
||||
* self.app.sweep_control.get_segments()
|
||||
)
|
||||
|
||||
self.app.sweep_control.set_segments(1) # speed up things
|
||||
self.app.sweep_control.update_center_span()
|
||||
|
|
|
@ -21,8 +21,10 @@ import logging
|
|||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency_sweep, format_frequency_short,
|
||||
parse_frequency)
|
||||
format_frequency_sweep,
|
||||
format_frequency_short,
|
||||
parse_frequency,
|
||||
)
|
||||
from NanoVNASaver.Inputs import FrequencyInputWidget
|
||||
from NanoVNASaver.Controls.Control import Control
|
||||
|
||||
|
@ -30,7 +32,6 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class SweepControl(Control):
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__(app, "Sweep control")
|
||||
|
||||
|
@ -66,8 +67,7 @@ class SweepControl(Control):
|
|||
self.input_center.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.input_center.textEdited.connect(self.update_start_end)
|
||||
|
||||
input_right_layout.addRow(QtWidgets.QLabel(
|
||||
"Center"), self.input_center)
|
||||
input_right_layout.addRow(QtWidgets.QLabel("Center"), self.input_center)
|
||||
|
||||
self.input_span = FrequencyInputWidget()
|
||||
self.input_span.setFixedHeight(20)
|
||||
|
@ -77,7 +77,8 @@ class SweepControl(Control):
|
|||
input_right_layout.addRow(QtWidgets.QLabel("Span"), self.input_span)
|
||||
|
||||
self.input_segments = QtWidgets.QLineEdit(
|
||||
self.app.settings.value("Segments", "1"))
|
||||
self.app.settings.value("Segments", "1")
|
||||
)
|
||||
self.input_segments.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.input_segments.setFixedHeight(20)
|
||||
self.input_segments.setFixedWidth(60)
|
||||
|
@ -85,7 +86,8 @@ class SweepControl(Control):
|
|||
|
||||
self.label_step = QtWidgets.QLabel("Hz/step")
|
||||
self.label_step.setAlignment(
|
||||
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
|
||||
)
|
||||
|
||||
segment_layout = QtWidgets.QHBoxLayout()
|
||||
segment_layout.addWidget(self.input_segments)
|
||||
|
@ -95,7 +97,8 @@ class SweepControl(Control):
|
|||
btn_settings_window = QtWidgets.QPushButton("Sweep settings ...")
|
||||
btn_settings_window.setFixedHeight(20)
|
||||
btn_settings_window.clicked.connect(
|
||||
lambda: self.app.display_window("sweep_settings"))
|
||||
lambda: self.app.display_window("sweep_settings")
|
||||
)
|
||||
|
||||
self.layout.addRow(btn_settings_window)
|
||||
|
||||
|
@ -206,8 +209,7 @@ class SweepControl(Control):
|
|||
segments = self.get_segments()
|
||||
if segments > 0:
|
||||
fstep = fspan / (segments * self.app.vna.datapoints - 1)
|
||||
self.label_step.setText(
|
||||
f"{format_frequency_short(fstep)}/step")
|
||||
self.label_step.setText(f"{format_frequency_short(fstep)}/step")
|
||||
self.update_sweep()
|
||||
|
||||
def update_sweep(self):
|
||||
|
|
|
@ -43,12 +43,12 @@ class GUI:
|
|||
|
||||
@DC.dataclass
|
||||
class ChartsSelected:
|
||||
chart_00: str = 'S11 Smith Chart'
|
||||
chart_01: str = 'S11 Return Loss'
|
||||
chart_02: str = 'None'
|
||||
chart_10: str = 'S21 Polar Plot'
|
||||
chart_11: str = 'S21 Gain'
|
||||
chart_12: str = 'None'
|
||||
chart_00: str = "S11 Smith Chart"
|
||||
chart_01: str = "S11 Return Loss"
|
||||
chart_02: str = "None"
|
||||
chart_10: str = "S21 Polar Plot"
|
||||
chart_11: str = "S21 Gain"
|
||||
chart_12: str = "None"
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
|
@ -69,33 +69,49 @@ class Chart:
|
|||
@DC.dataclass
|
||||
class ChartColors: # pylint: disable=too-many-instance-attributes
|
||||
background: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.white))
|
||||
default_factory=lambda: QColor(QtCore.Qt.white)
|
||||
)
|
||||
foreground: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray)
|
||||
)
|
||||
reference: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255, 64))
|
||||
reference_secondary: QColor = DC.field(
|
||||
default_factory=lambda: QColor(0, 0, 192, 48))
|
||||
default_factory=lambda: QColor(0, 0, 192, 48)
|
||||
)
|
||||
sweep: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkYellow))
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkYellow)
|
||||
)
|
||||
sweep_secondary: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkMagenta))
|
||||
swr: QColor = DC.field(
|
||||
default_factory=lambda: QColor(255, 0, 0, 128))
|
||||
text: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.black))
|
||||
bands: QColor = DC.field(
|
||||
default_factory=lambda: QColor(128, 128, 128, 48))
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkMagenta)
|
||||
)
|
||||
swr: QColor = DC.field(default_factory=lambda: QColor(255, 0, 0, 128))
|
||||
text: QColor = DC.field(default_factory=lambda: QColor(QtCore.Qt.black))
|
||||
bands: QColor = DC.field(default_factory=lambda: QColor(128, 128, 128, 48))
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
class Markers:
|
||||
active_labels: list = DC.field(default_factory=lambda: [
|
||||
"actualfreq", "impedance", "serr", "serl", "serc", "parr", "parlc",
|
||||
"vswr", "returnloss", "s11q", "s11phase", "s21gain", "s21phase",
|
||||
])
|
||||
active_labels: list = DC.field(
|
||||
default_factory=lambda: [
|
||||
"actualfreq",
|
||||
"impedance",
|
||||
"serr",
|
||||
"serl",
|
||||
"serc",
|
||||
"parr",
|
||||
"parlc",
|
||||
"vswr",
|
||||
"returnloss",
|
||||
"s11q",
|
||||
"s11phase",
|
||||
"s21gain",
|
||||
"s21phase",
|
||||
]
|
||||
)
|
||||
colored_names: bool = True
|
||||
color_0: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkGray))
|
||||
default_factory=lambda: QColor(QtCore.Qt.darkGray)
|
||||
)
|
||||
color_1: QColor = DC.field(default_factory=lambda: QColor(255, 0, 0))
|
||||
color_2: QColor = DC.field(default_factory=lambda: QColor(0, 255, 0))
|
||||
color_3: QColor = DC.field(default_factory=lambda: QColor(0, 0, 255))
|
||||
|
@ -103,37 +119,34 @@ class Markers:
|
|||
color_5: QColor = DC.field(default_factory=lambda: QColor(255, 0, 255))
|
||||
color_6: QColor = DC.field(default_factory=lambda: QColor(255, 255, 0))
|
||||
color_7: QColor = DC.field(
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray))
|
||||
default_factory=lambda: QColor(QtCore.Qt.lightGray)
|
||||
)
|
||||
|
||||
|
||||
@DC.dataclass
|
||||
class CFG:
|
||||
gui: object = DC.field(
|
||||
default_factory=lambda: GUI())
|
||||
charts_selected: object = DC.field(
|
||||
default_factory=lambda: ChartsSelected())
|
||||
chart: object = DC.field(
|
||||
default_factory=lambda: Chart())
|
||||
chart_colors: object = DC.field(
|
||||
default_factory=lambda: ChartColors())
|
||||
markers: object = DC.field(
|
||||
default_factory=lambda: Markers())
|
||||
gui: object = DC.field(default_factory=lambda: GUI())
|
||||
charts_selected: object = DC.field(default_factory=lambda: ChartsSelected())
|
||||
chart: object = DC.field(default_factory=lambda: Chart())
|
||||
chart_colors: object = DC.field(default_factory=lambda: ChartColors())
|
||||
markers: object = DC.field(default_factory=lambda: Markers())
|
||||
|
||||
|
||||
cfg = CFG()
|
||||
|
||||
|
||||
def restore(settings: 'AppSettings') -> CFG:
|
||||
def restore(settings: "AppSettings") -> CFG:
|
||||
result = CFG()
|
||||
for field in DC.fields(result):
|
||||
value = settings.restore_dataclass(field.name.upper(),
|
||||
getattr(result, field.name))
|
||||
value = settings.restore_dataclass(
|
||||
field.name.upper(), getattr(result, field.name)
|
||||
)
|
||||
setattr(result, field.name, value)
|
||||
logger.debug("restored\n(\n%s\n)", result)
|
||||
return result
|
||||
|
||||
|
||||
def store(settings: 'AppSettings', data: CFG = None) -> None:
|
||||
def store(settings: "AppSettings", data: CFG = None) -> None:
|
||||
data = data or cfg
|
||||
logger.debug("storing\n(\n%s\n)", data)
|
||||
assert isinstance(data, CFG)
|
||||
|
@ -147,25 +160,25 @@ def from_type(data) -> str:
|
|||
type_map = {
|
||||
bytearray: lambda x: x.hex(),
|
||||
QColor: lambda x: x.getRgb(),
|
||||
QByteArray: lambda x: x.toHex()
|
||||
QByteArray: lambda x: x.toHex(),
|
||||
}
|
||||
return (f"{type_map[type(data)](data)}" if
|
||||
type(data) in type_map else
|
||||
f"{data}")
|
||||
return (
|
||||
f"{type_map[type(data)](data)}" if type(data) in type_map else f"{data}"
|
||||
)
|
||||
|
||||
|
||||
def to_type(data: object, data_type: type) -> object:
|
||||
type_map = {
|
||||
bool: lambda x: x.lower() == 'true',
|
||||
bool: lambda x: x.lower() == "true",
|
||||
bytearray: bytearray.fromhex,
|
||||
list: literal_eval,
|
||||
tuple: literal_eval,
|
||||
QColor: lambda x: QColor.fromRgb(*literal_eval(x)),
|
||||
QByteArray: lambda x: QByteArray.fromHex(literal_eval(x))
|
||||
QByteArray: lambda x: QByteArray.fromHex(literal_eval(x)),
|
||||
}
|
||||
return (type_map[data_type](data) if
|
||||
data_type in type_map else
|
||||
data_type(data))
|
||||
return (
|
||||
type_map[data_type](data) if data_type in type_map else data_type(data)
|
||||
)
|
||||
|
||||
|
||||
# noinspection PyDataclass
|
||||
|
@ -178,8 +191,13 @@ class AppSettings(QSettings):
|
|||
try:
|
||||
assert isinstance(value, field.type)
|
||||
except AssertionError as exc:
|
||||
logger.error("%s: %s of type %s is not a %s",
|
||||
name, field.name, type(value), field.type)
|
||||
logger.error(
|
||||
"%s: %s of type %s is not a %s",
|
||||
name,
|
||||
field.name,
|
||||
type(value),
|
||||
field.type,
|
||||
)
|
||||
raise TypeError from exc
|
||||
self.setValue(field.name, from_type(value))
|
||||
self.endGroup()
|
||||
|
|
|
@ -27,22 +27,27 @@ FMT_FREQ_SHORT = SITools.Format(max_nr_digits=4)
|
|||
FMT_FREQ_SPACE = SITools.Format(space_str=" ")
|
||||
FMT_FREQ_SWEEP = SITools.Format(max_nr_digits=9, allow_strip=True)
|
||||
FMT_FREQ_INPUTS = SITools.Format(
|
||||
max_nr_digits=10, allow_strip=True,
|
||||
printable_min=0, unprintable_under="- ")
|
||||
max_nr_digits=10, allow_strip=True, printable_min=0, unprintable_under="- "
|
||||
)
|
||||
FMT_Q_FACTOR = SITools.Format(
|
||||
max_nr_digits=4, assume_infinity=False,
|
||||
min_offset=0, max_offset=0, allow_strip=True)
|
||||
max_nr_digits=4,
|
||||
assume_infinity=False,
|
||||
min_offset=0,
|
||||
max_offset=0,
|
||||
allow_strip=True,
|
||||
)
|
||||
FMT_GROUP_DELAY = SITools.Format(max_nr_digits=5, space_str=" ")
|
||||
FMT_REACT = SITools.Format(max_nr_digits=5, space_str=" ", allow_strip=True)
|
||||
FMT_COMPLEX = SITools.Format(max_nr_digits=3, allow_strip=True,
|
||||
printable_min=0, unprintable_under="- ")
|
||||
FMT_COMPLEX = SITools.Format(
|
||||
max_nr_digits=3, allow_strip=True, printable_min=0, unprintable_under="- "
|
||||
)
|
||||
FMT_COMPLEX_NEG = SITools.Format(max_nr_digits=3, allow_strip=True)
|
||||
FMT_SHORT = SITools.Format(max_nr_digits=4)
|
||||
FMT_WAVELENGTH = SITools.Format(max_nr_digits=4, space_str=" ")
|
||||
FMT_PARSE = SITools.Format(parse_sloppy_unit=True, parse_sloppy_kilo=True,
|
||||
parse_clamp_min=0)
|
||||
FMT_PARSE_VALUE = SITools.Format(
|
||||
parse_sloppy_unit=True, parse_sloppy_kilo=True)
|
||||
FMT_PARSE = SITools.Format(
|
||||
parse_sloppy_unit=True, parse_sloppy_kilo=True, parse_clamp_min=0
|
||||
)
|
||||
FMT_PARSE_VALUE = SITools.Format(parse_sloppy_unit=True, parse_sloppy_kilo=True)
|
||||
FMT_VSWR = SITools.Format(max_nr_digits=3)
|
||||
|
||||
|
||||
|
@ -117,7 +122,7 @@ def format_group_delay(val: float) -> str:
|
|||
|
||||
|
||||
def format_phase(val: float) -> str:
|
||||
return f"{math.degrees(val):.2f}""\N{DEGREE SIGN}"
|
||||
return f"{math.degrees(val):.2f}" "\N{DEGREE SIGN}"
|
||||
|
||||
|
||||
def format_complex_adm(z: complex, allow_negative: bool = False) -> str:
|
||||
|
@ -135,7 +140,7 @@ def format_complex_imp(z: complex, allow_negative: bool = False) -> str:
|
|||
fmt_re = FMT_COMPLEX_NEG if allow_negative else FMT_COMPLEX
|
||||
re = SITools.Value(z.real, fmt=fmt_re)
|
||||
im = SITools.Value(abs(z.imag), fmt=FMT_COMPLEX)
|
||||
return f"{re}{'-' if z.imag < 0 else '+'}j{im} ""\N{OHM SIGN}"
|
||||
return f"{re}{'-' if z.imag < 0 else '+'}j{im} " "\N{OHM SIGN}"
|
||||
|
||||
|
||||
def format_wavelength(length: Number) -> str:
|
||||
|
@ -153,10 +158,11 @@ def parse_frequency(freq: str) -> int:
|
|||
return -1
|
||||
|
||||
|
||||
def parse_value(val: str, unit: str = "",
|
||||
fmt: SITools.Format = FMT_PARSE_VALUE) -> float:
|
||||
def parse_value(
|
||||
val: str, unit: str = "", fmt: SITools.Format = FMT_PARSE_VALUE
|
||||
) -> float:
|
||||
try:
|
||||
val.replace(',', '.')
|
||||
val.replace(",", ".")
|
||||
return float(SITools.Value(val, unit, fmt))
|
||||
except (ValueError, IndexError):
|
||||
return 0.0
|
||||
|
|
|
@ -43,8 +43,8 @@ USBDevice = namedtuple("Device", "vid pid name")
|
|||
|
||||
USBDEVICETYPES = (
|
||||
USBDevice(0x0483, 0x5740, "NanoVNA"),
|
||||
USBDevice(0x16c0, 0x0483, "AVNA"),
|
||||
USBDevice(0x04b4, 0x0008, "S-A-A-2"),
|
||||
USBDevice(0x16C0, 0x0483, "AVNA"),
|
||||
USBDevice(0x04B4, 0x0008, "S-A-A-2"),
|
||||
)
|
||||
RETRIES = 3
|
||||
TIMEOUT = 0.2
|
||||
|
@ -71,15 +71,21 @@ NAME2DEVICE = {
|
|||
|
||||
def _fix_v2_hwinfo(dev):
|
||||
# if dev.hwid == r'PORTS\VID_04B4&PID_0008\DEMO':
|
||||
if r'PORTS\VID_04B4&PID_0008' in dev.hwid:
|
||||
dev.vid, dev.pid = 0x04b4, 0x0008
|
||||
if r"PORTS\VID_04B4&PID_0008" in dev.hwid:
|
||||
dev.vid, dev.pid = 0x04B4, 0x0008
|
||||
return dev
|
||||
|
||||
|
||||
def usb_typename(device: ListPortInfo) -> str:
|
||||
return next((t.name for t in USBDEVICETYPES if
|
||||
device.vid == t.vid and device.pid == t.pid),
|
||||
"")
|
||||
return next(
|
||||
(
|
||||
t.name
|
||||
for t in USBDEVICETYPES
|
||||
if device.vid == t.vid and device.pid == t.pid
|
||||
),
|
||||
"",
|
||||
)
|
||||
|
||||
|
||||
# Get list of interfaces with VNAs connected
|
||||
|
||||
|
@ -88,13 +94,18 @@ def get_interfaces() -> List[Interface]:
|
|||
interfaces = []
|
||||
# serial like usb interfaces
|
||||
for d in list_ports.comports():
|
||||
if platform.system() == 'Windows' and d.vid is None:
|
||||
if platform.system() == "Windows" and d.vid is None:
|
||||
d = _fix_v2_hwinfo(d)
|
||||
if not (typename := usb_typename(d)):
|
||||
continue
|
||||
logger.debug("Found %s USB:(%04x:%04x) on port %s",
|
||||
typename, d.vid, d.pid, d.device)
|
||||
iface = Interface('serial', typename)
|
||||
logger.debug(
|
||||
"Found %s USB:(%04x:%04x) on port %s",
|
||||
typename,
|
||||
d.vid,
|
||||
d.pid,
|
||||
d.device,
|
||||
)
|
||||
iface = Interface("serial", typename)
|
||||
iface.port = d.device
|
||||
iface.open()
|
||||
iface.comment = get_comment(iface)
|
||||
|
@ -109,9 +120,8 @@ def get_portinfos() -> List[str]:
|
|||
portinfos = []
|
||||
# serial like usb interfaces
|
||||
for d in list_ports.comports():
|
||||
logger.debug("Found USB:(%04x:%04x) on port %s",
|
||||
d.vid, d.pid, d.device)
|
||||
iface = Interface('serial', "DEBUG")
|
||||
logger.debug("Found USB:(%04x:%04x) on port %s", d.vid, d.pid, d.device)
|
||||
iface = Interface("serial", "DEBUG")
|
||||
iface.port = d.device
|
||||
iface.open()
|
||||
version = detect_version(iface)
|
||||
|
@ -130,19 +140,19 @@ def get_comment(iface: Interface) -> str:
|
|||
with iface.lock:
|
||||
vna_version = detect_version(iface)
|
||||
|
||||
if vna_version == 'v2':
|
||||
if vna_version == "v2":
|
||||
return "S-A-A-2"
|
||||
|
||||
logger.info("Finding firmware variant...")
|
||||
info = get_info(iface)
|
||||
for search, name in (
|
||||
("AVNA + Teensy", "AVNA"),
|
||||
("NanoVNA-H 4", "H4"),
|
||||
("NanoVNA-H", "H"),
|
||||
("NanoVNA-F_V2", "F_V2"),
|
||||
("NanoVNA-F", "F"),
|
||||
("NanoVNA", "NanoVNA"),
|
||||
("tinySA", "tinySA"),
|
||||
("AVNA + Teensy", "AVNA"),
|
||||
("NanoVNA-H 4", "H4"),
|
||||
("NanoVNA-H", "H"),
|
||||
("NanoVNA-F_V2", "F_V2"),
|
||||
("NanoVNA-F", "F"),
|
||||
("NanoVNA", "NanoVNA"),
|
||||
("tinySA", "tinySA"),
|
||||
):
|
||||
if info.find(search) >= 0:
|
||||
return name
|
||||
|
@ -171,7 +181,7 @@ def detect_version(serial_port: serial.Serial) -> str:
|
|||
if data.startswith("2"):
|
||||
return "v2"
|
||||
logger.debug("Retry detection: %s", i + 1)
|
||||
logger.error('No VNA detected. Hardware responded to CR with: %s', data)
|
||||
logger.error("No VNA detected. Hardware responded to CR with: %s", data)
|
||||
return ""
|
||||
|
||||
|
||||
|
|
|
@ -46,7 +46,6 @@ class NanoVNA(VNA):
|
|||
self._sweepdata = []
|
||||
|
||||
def _get_running_frequencies(self):
|
||||
|
||||
logger.debug("Reading values: frequencies")
|
||||
try:
|
||||
frequencies = super().readValues("frequencies")
|
||||
|
@ -61,24 +60,27 @@ class NanoVNA(VNA):
|
|||
timeout = self.serial.timeout
|
||||
with self.serial.lock:
|
||||
drain_serial(self.serial)
|
||||
self.serial.write("capture\r".encode('ascii'))
|
||||
self.serial.write("capture\r".encode("ascii"))
|
||||
self.serial.readline()
|
||||
self.serial.timeout = 4
|
||||
image_data = self.serial.read(
|
||||
self.screenwidth * self.screenheight * 2)
|
||||
self.screenwidth * self.screenheight * 2
|
||||
)
|
||||
self.serial.timeout = timeout
|
||||
self.serial.timeout = timeout
|
||||
return image_data
|
||||
|
||||
def _convert_data(self, image_data: bytes) -> bytes:
|
||||
rgb_data = struct.unpack(
|
||||
f">{self.screenwidth * self.screenheight}H",
|
||||
image_data)
|
||||
f">{self.screenwidth * self.screenheight}H", image_data
|
||||
)
|
||||
rgb_array = np.array(rgb_data, dtype=np.uint32)
|
||||
return (0xFF000000 +
|
||||
((rgb_array & 0xF800) << 8) +
|
||||
((rgb_array & 0x07E0) << 5) +
|
||||
((rgb_array & 0x001F) << 3))
|
||||
return (
|
||||
0xFF000000
|
||||
+ ((rgb_array & 0xF800) << 8)
|
||||
+ ((rgb_array & 0x07E0) << 5)
|
||||
+ ((rgb_array & 0x001F) << 3)
|
||||
)
|
||||
|
||||
def getScreenshot(self) -> QtGui.QPixmap:
|
||||
logger.debug("Capturing screenshot...")
|
||||
|
@ -90,12 +92,12 @@ class NanoVNA(VNA):
|
|||
rgba_array,
|
||||
self.screenwidth,
|
||||
self.screenheight,
|
||||
QtGui.QImage.Format_ARGB32)
|
||||
QtGui.QImage.Format_ARGB32,
|
||||
)
|
||||
logger.debug("Captured screenshot")
|
||||
return QtGui.QPixmap(image)
|
||||
except serial.SerialException as exc:
|
||||
logger.exception(
|
||||
"Exception while capturing screenshot: %s", exc)
|
||||
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||
return QtGui.QPixmap()
|
||||
|
||||
def resetSweep(self, start: int, stop: int):
|
||||
|
@ -125,8 +127,12 @@ class NanoVNA(VNA):
|
|||
logger.debug("readFrequencies: %s", self.sweep_method)
|
||||
if self.sweep_method != "scan_mask":
|
||||
return super().readFrequencies()
|
||||
return [int(line) for line in self.exec_command(
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b001")]
|
||||
return [
|
||||
int(line)
|
||||
for line in self.exec_command(
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b001"
|
||||
)
|
||||
]
|
||||
|
||||
def readValues(self, value) -> List[str]:
|
||||
if self.sweep_method != "scan_mask":
|
||||
|
@ -137,11 +143,12 @@ class NanoVNA(VNA):
|
|||
if value == "data 0":
|
||||
self._sweepdata = []
|
||||
for line in self.exec_command(
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b110"):
|
||||
f"scan {self.start} {self.stop} {self.datapoints} 0b110"
|
||||
):
|
||||
data = line.split()
|
||||
self._sweepdata.append((
|
||||
f"{data[0]} {data[1]}",
|
||||
f"{data[2]} {data[3]}"))
|
||||
self._sweepdata.append(
|
||||
(f"{data[0]} {data[1]}", f"{data[2]} {data[3]}")
|
||||
)
|
||||
if value == "data 0":
|
||||
return [x[0] for x in self._sweepdata]
|
||||
if value == "data 1":
|
||||
|
|
|
@ -46,10 +46,10 @@ class NanoVNA_F_V2(NanoVNA):
|
|||
rgba_array,
|
||||
self.screenwidth,
|
||||
self.screenheight,
|
||||
QtGui.QImage.Format_RGB16)
|
||||
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)
|
||||
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||
return QtGui.QPixmap()
|
||||
|
|
|
@ -26,13 +26,13 @@ from NanoVNASaver.Hardware.Serial import Interface
|
|||
from NanoVNASaver.Hardware.VNA import VNA
|
||||
from NanoVNASaver.Version import Version
|
||||
|
||||
if platform.system() != 'Windows':
|
||||
if platform.system() != "Windows":
|
||||
import tty
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_CMD_NOP = 0x00
|
||||
_CMD_INDICATE = 0x0d
|
||||
_CMD_INDICATE = 0x0D
|
||||
_CMD_READ = 0x10
|
||||
_CMD_READ2 = 0x11
|
||||
_CMD_READ4 = 0x12
|
||||
|
@ -49,22 +49,23 @@ _ADDR_SWEEP_POINTS = 0x20
|
|||
_ADDR_SWEEP_VALS_PER_FREQ = 0x22
|
||||
_ADDR_RAW_SAMPLES_MODE = 0x26
|
||||
_ADDR_VALUES_FIFO = 0x30
|
||||
_ADDR_DEVICE_VARIANT = 0xf0
|
||||
_ADDR_PROTOCOL_VERSION = 0xf1
|
||||
_ADDR_HARDWARE_REVISION = 0xf2
|
||||
_ADDR_FW_MAJOR = 0xf3
|
||||
_ADDR_FW_MINOR = 0xf4
|
||||
_ADDR_DEVICE_VARIANT = 0xF0
|
||||
_ADDR_PROTOCOL_VERSION = 0xF1
|
||||
_ADDR_HARDWARE_REVISION = 0xF2
|
||||
_ADDR_FW_MAJOR = 0xF3
|
||||
_ADDR_FW_MINOR = 0xF4
|
||||
|
||||
WRITE_SLEEP = 0.05
|
||||
|
||||
_ADF4350_TXPOWER_DESC_MAP = {
|
||||
0: '9dB attenuation',
|
||||
1: '6dB attenuation',
|
||||
2: '3dB attenuation',
|
||||
3: 'Maximum',
|
||||
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()}
|
||||
value: key for key, value in _ADF4350_TXPOWER_DESC_MAP.items()
|
||||
}
|
||||
|
||||
|
||||
class NanoVNA_V2(VNA):
|
||||
|
@ -76,7 +77,7 @@ class NanoVNA_V2(VNA):
|
|||
def __init__(self, iface: Interface):
|
||||
super().__init__(iface)
|
||||
|
||||
if platform.system() != 'Windows':
|
||||
if platform.system() != "Windows":
|
||||
tty.setraw(self.serial.fd)
|
||||
|
||||
# reset protocol to known state
|
||||
|
@ -85,8 +86,8 @@ class NanoVNA_V2(VNA):
|
|||
sleep(WRITE_SLEEP)
|
||||
|
||||
# firmware major version of 0xff indicates dfu mode
|
||||
if self.version.data["major"] == 0xff:
|
||||
raise IOError('Device is in DFU mode')
|
||||
if self.version.data["major"] == 0xFF:
|
||||
raise IOError("Device is in DFU mode")
|
||||
|
||||
if "S21 hack" in self.features:
|
||||
self.valid_datapoints = (101, 11, 51, 201, 301, 501, 1021)
|
||||
|
@ -116,8 +117,13 @@ class NanoVNA_V2(VNA):
|
|||
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)]),
|
||||
(
|
||||
(140e6, self.sweep_max_freq_Hz),
|
||||
[
|
||||
_ADF4350_TXPOWER_DESC_MAP[value]
|
||||
for value in (3, 2, 1, 0)
|
||||
],
|
||||
),
|
||||
]
|
||||
|
||||
def readFirmware(self) -> str:
|
||||
|
@ -135,9 +141,15 @@ class NanoVNA_V2(VNA):
|
|||
freq_index = -1
|
||||
|
||||
for i in range(pointstoread):
|
||||
(fwd_real, fwd_imag, rev0_real, rev0_imag, rev1_real,
|
||||
rev1_imag, freq_index) = unpack_from(
|
||||
"<iiiiiihxxxxxx", arr, i * 32)
|
||||
(
|
||||
fwd_real,
|
||||
fwd_imag,
|
||||
rev0_real,
|
||||
rev0_imag,
|
||||
rev1_real,
|
||||
rev1_imag,
|
||||
freq_index,
|
||||
) = unpack_from("<iiiiiihxxxxxx", arr, i * 32)
|
||||
fwd = complex(fwd_real, fwd_imag)
|
||||
refl = complex(rev0_real, rev0_imag)
|
||||
thru = complex(rev1_real, rev1_imag)
|
||||
|
@ -158,12 +170,14 @@ class NanoVNA_V2(VNA):
|
|||
self.serial.write(pack("<Q", 0))
|
||||
sleep(WRITE_SLEEP)
|
||||
# cmd: write register 0x30 to clear FIFO
|
||||
self.serial.write(pack("<BBB",
|
||||
_CMD_WRITE, _ADDR_VALUES_FIFO, 0))
|
||||
self.serial.write(
|
||||
pack("<BBB", _CMD_WRITE, _ADDR_VALUES_FIFO, 0)
|
||||
)
|
||||
sleep(WRITE_SLEEP)
|
||||
# clear sweepdata
|
||||
self._sweepdata = [(complex(), complex())] * (
|
||||
self.datapoints + s21hack)
|
||||
self.datapoints + s21hack
|
||||
)
|
||||
pointstodo = self.datapoints + s21hack
|
||||
# we read at most 255 values at a time and the time required
|
||||
# empirically is just over 3 seconds for 101 points or
|
||||
|
@ -174,9 +188,13 @@ class NanoVNA_V2(VNA):
|
|||
pointstoread = min(255, pointstodo)
|
||||
# cmd: read FIFO, addr 0x30
|
||||
self.serial.write(
|
||||
pack("<BBB",
|
||||
_CMD_READFIFO, _ADDR_VALUES_FIFO,
|
||||
pointstoread))
|
||||
pack(
|
||||
"<BBB",
|
||||
_CMD_READFIFO,
|
||||
_ADDR_VALUES_FIFO,
|
||||
pointstoread,
|
||||
)
|
||||
)
|
||||
sleep(WRITE_SLEEP)
|
||||
# each value is 32 bytes
|
||||
nBytes = pointstoread * 32
|
||||
|
@ -185,8 +203,9 @@ class NanoVNA_V2(VNA):
|
|||
# timeout secs
|
||||
arr = self.serial.read(nBytes)
|
||||
if nBytes != len(arr):
|
||||
logger.warning("expected %d bytes, got %d",
|
||||
nBytes, len(arr))
|
||||
logger.warning(
|
||||
"expected %d bytes, got %d", nBytes, len(arr)
|
||||
)
|
||||
# the way to retry on timeout is keep the data
|
||||
# already read then try to read the rest of
|
||||
# the data into the array
|
||||
|
@ -205,8 +224,7 @@ class NanoVNA_V2(VNA):
|
|||
|
||||
idx = 1 if value == "data 1" else 0
|
||||
return [
|
||||
f'{str(x[idx].real)} {str(x[idx].imag)}'
|
||||
for x in self._sweepdata
|
||||
f"{str(x[idx].real)} {str(x[idx].imag)}" for x in self._sweepdata
|
||||
]
|
||||
|
||||
def resetSweep(self, start: int, stop: int):
|
||||
|
@ -225,15 +243,15 @@ class NanoVNA_V2(VNA):
|
|||
raise IOError("Timeout reading version registers")
|
||||
return Version(f"{resp[0]}.0.{resp[1]}")
|
||||
|
||||
def readVersion(self) -> 'Version':
|
||||
result = self._read_version(_ADDR_FW_MAJOR,
|
||||
_ADDR_FW_MINOR)
|
||||
def readVersion(self) -> "Version":
|
||||
result = self._read_version(_ADDR_FW_MAJOR, _ADDR_FW_MINOR)
|
||||
logger.debug("readVersion: %s", result)
|
||||
return result
|
||||
|
||||
def read_board_revision(self) -> 'Version':
|
||||
result = self._read_version(_ADDR_DEVICE_VARIANT,
|
||||
_ADDR_HARDWARE_REVISION)
|
||||
def read_board_revision(self) -> "Version":
|
||||
result = self._read_version(
|
||||
_ADDR_DEVICE_VARIANT, _ADDR_HARDWARE_REVISION
|
||||
)
|
||||
logger.debug("read_board_revision: %s", result)
|
||||
return result
|
||||
|
||||
|
@ -243,34 +261,41 @@ class NanoVNA_V2(VNA):
|
|||
return
|
||||
self.sweepStartHz = start
|
||||
self.sweepStepHz = step
|
||||
logger.info('NanoVNAV2: set sweep start %d step %d',
|
||||
self.sweepStartHz, self.sweepStepHz)
|
||||
logger.info(
|
||||
"NanoVNAV2: set sweep start %d step %d",
|
||||
self.sweepStartHz,
|
||||
self.sweepStepHz,
|
||||
)
|
||||
self._updateSweep()
|
||||
return
|
||||
|
||||
def _updateSweep(self):
|
||||
s21hack = "S21 hack" in self.features
|
||||
cmd = pack("<BBQ", _CMD_WRITE8, _ADDR_SWEEP_START,
|
||||
max(50000,
|
||||
int(self.sweepStartHz - (self.sweepStepHz * s21hack))))
|
||||
cmd += pack("<BBQ", _CMD_WRITE8,
|
||||
_ADDR_SWEEP_STEP, int(self.sweepStepHz))
|
||||
cmd += pack("<BBH", _CMD_WRITE2,
|
||||
_ADDR_SWEEP_POINTS, self.datapoints + s21hack)
|
||||
cmd += pack("<BBH", _CMD_WRITE2,
|
||||
_ADDR_SWEEP_VALS_PER_FREQ, 1)
|
||||
cmd = pack(
|
||||
"<BBQ",
|
||||
_CMD_WRITE8,
|
||||
_ADDR_SWEEP_START,
|
||||
max(50000, int(self.sweepStartHz - (self.sweepStepHz * s21hack))),
|
||||
)
|
||||
cmd += pack(
|
||||
"<BBQ", _CMD_WRITE8, _ADDR_SWEEP_STEP, int(self.sweepStepHz)
|
||||
)
|
||||
cmd += pack(
|
||||
"<BBH", _CMD_WRITE2, _ADDR_SWEEP_POINTS, self.datapoints + s21hack
|
||||
)
|
||||
cmd += pack("<BBH", _CMD_WRITE2, _ADDR_SWEEP_VALS_PER_FREQ, 1)
|
||||
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')
|
||||
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):
|
||||
packet = b''
|
||||
packet = b""
|
||||
if size == 1:
|
||||
packet = pack("<BBB", _CMD_WRITE, addr, value)
|
||||
elif size == 2:
|
||||
|
|
|
@ -41,7 +41,7 @@ def drain_serial(serial_port: serial.Serial):
|
|||
class Interface(serial.Serial):
|
||||
def __init__(self, interface_type: str, comment, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
assert interface_type in {'serial', 'usb', 'bt', 'network'}
|
||||
assert interface_type in {"serial", "usb", "bt", "network"}
|
||||
self.type = interface_type
|
||||
self.comment = comment
|
||||
self.port = None
|
||||
|
|
|
@ -34,18 +34,17 @@ class TinySA(VNA):
|
|||
name = "tinySA"
|
||||
screenwidth = 320
|
||||
screenheight = 240
|
||||
valid_datapoints = (290, )
|
||||
valid_datapoints = (290,)
|
||||
|
||||
def __init__(self, iface: Interface):
|
||||
super().__init__(iface)
|
||||
self.features = {'Screenshots'}
|
||||
self.features = {"Screenshots"}
|
||||
logger.debug("Setting initial start,stop")
|
||||
self.start, self.stop = self._get_running_frequencies()
|
||||
self.sweep_max_freq_Hz = 950e6
|
||||
self._sweepdata = []
|
||||
|
||||
def _get_running_frequencies(self):
|
||||
|
||||
logger.debug("Reading values: frequencies")
|
||||
try:
|
||||
frequencies = super().readValues("frequencies")
|
||||
|
@ -60,24 +59,27 @@ class TinySA(VNA):
|
|||
timeout = self.serial.timeout
|
||||
with self.serial.lock:
|
||||
drain_serial(self.serial)
|
||||
self.serial.write("capture\r".encode('ascii'))
|
||||
self.serial.write("capture\r".encode("ascii"))
|
||||
self.serial.readline()
|
||||
self.serial.timeout = 4
|
||||
image_data = self.serial.read(
|
||||
self.screenwidth * self.screenheight * 2)
|
||||
self.screenwidth * self.screenheight * 2
|
||||
)
|
||||
self.serial.timeout = timeout
|
||||
self.serial.timeout = timeout
|
||||
return image_data
|
||||
|
||||
def _convert_data(self, image_data: bytes) -> bytes:
|
||||
rgb_data = struct.unpack(
|
||||
f">{self.screenwidth * self.screenheight}H",
|
||||
image_data)
|
||||
f">{self.screenwidth * self.screenheight}H", image_data
|
||||
)
|
||||
rgb_array = np.array(rgb_data, dtype=np.uint32)
|
||||
return (0xFF000000 +
|
||||
((rgb_array & 0xF800) << 8) +
|
||||
((rgb_array & 0x07E0) << 5) +
|
||||
((rgb_array & 0x001F) << 3))
|
||||
return (
|
||||
0xFF000000
|
||||
+ ((rgb_array & 0xF800) << 8)
|
||||
+ ((rgb_array & 0x07E0) << 5)
|
||||
+ ((rgb_array & 0x001F) << 3)
|
||||
)
|
||||
|
||||
def getScreenshot(self) -> QtGui.QPixmap:
|
||||
logger.debug("Capturing screenshot...")
|
||||
|
@ -89,12 +91,12 @@ class TinySA(VNA):
|
|||
rgba_array,
|
||||
self.screenwidth,
|
||||
self.screenheight,
|
||||
QtGui.QImage.Format_ARGB32)
|
||||
QtGui.QImage.Format_ARGB32,
|
||||
)
|
||||
logger.debug("Captured screenshot")
|
||||
return QtGui.QPixmap(image)
|
||||
except serial.SerialException as exc:
|
||||
logger.exception(
|
||||
"Exception while capturing screenshot: %s", exc)
|
||||
logger.exception("Exception while capturing screenshot: %s", exc)
|
||||
return QtGui.QPixmap()
|
||||
|
||||
def resetSweep(self, start: int, stop: int):
|
||||
|
@ -113,6 +115,7 @@ class TinySA(VNA):
|
|||
def readValues(self, value) -> List[str]:
|
||||
logger.debug("Read: %s", value)
|
||||
if value == "data 0":
|
||||
self._sweepdata = [f"0 {line.strip()}"
|
||||
for line in self.exec_command("data")]
|
||||
self._sweepdata = [
|
||||
f"0 {line.strip()}" for line in self.exec_command("data")
|
||||
]
|
||||
return self._sweepdata
|
||||
|
|
|
@ -44,8 +44,11 @@ WAIT = 0.05
|
|||
|
||||
|
||||
def _max_retries(bandwidth: int, datapoints: int) -> int:
|
||||
return round(20 + 20 * (datapoints / 101) +
|
||||
(1000 / bandwidth) ** 1.30 * (datapoints / 101))
|
||||
return round(
|
||||
20
|
||||
+ 20 * (datapoints / 101)
|
||||
+ (1000 / bandwidth) ** 1.30 * (datapoints / 101)
|
||||
)
|
||||
|
||||
|
||||
class VNA:
|
||||
|
@ -94,7 +97,7 @@ class VNA:
|
|||
logger.debug("exec_command(%s)", command)
|
||||
with self.serial.lock:
|
||||
drain_serial(self.serial)
|
||||
self.serial.write(f"{command}\r".encode('ascii'))
|
||||
self.serial.write(f"{command}\r".encode("ascii"))
|
||||
sleep(wait)
|
||||
retries = 0
|
||||
max_retries = _max_retries(self.bandwidth, self.datapoints)
|
||||
|
@ -137,11 +140,14 @@ class VNA:
|
|||
result = result.split(" {")[1].strip("}")
|
||||
return sorted([int(i) for i in result.split("|")])
|
||||
except IndexError:
|
||||
return [1000, ]
|
||||
return [
|
||||
1000,
|
||||
]
|
||||
|
||||
def set_bandwidth(self, bandwidth: int):
|
||||
bw_val = DISLORD_BW[bandwidth] \
|
||||
if self.bw_method == "dislord" else bandwidth
|
||||
bw_val = (
|
||||
DISLORD_BW[bandwidth] if self.bw_method == "dislord" else bandwidth
|
||||
)
|
||||
result = " ".join(self.exec_command(f"bandwidth {bw_val}"))
|
||||
if self.bw_method == "ttrftech" and result:
|
||||
raise IOError(f"set_bandwith({bandwidth}: {result}")
|
||||
|
@ -191,11 +197,10 @@ class VNA:
|
|||
def readValues(self, value) -> List[str]:
|
||||
logger.debug("VNA reading %s", value)
|
||||
result = list(self.exec_command(value))
|
||||
logger.debug("VNA done reading %s (%d values)",
|
||||
value, len(result))
|
||||
logger.debug("VNA done reading %s (%d values)", value, len(result))
|
||||
return result
|
||||
|
||||
def readVersion(self) -> 'Version':
|
||||
def readVersion(self) -> "Version":
|
||||
result = list(self.exec_command("version"))
|
||||
logger.debug("result:\n%s", result)
|
||||
return Version(result[0])
|
||||
|
|
|
@ -61,71 +61,91 @@ class DeltaMarker(Marker):
|
|||
imp = imp_b - imp_a
|
||||
|
||||
cap_str = format_capacitance(
|
||||
RFTools.impedance_to_capacitance(imp_b, s11_b.freq) -
|
||||
RFTools.impedance_to_capacitance(imp_a, s11_a.freq))
|
||||
RFTools.impedance_to_capacitance(imp_b, s11_b.freq)
|
||||
- RFTools.impedance_to_capacitance(imp_a, s11_a.freq)
|
||||
)
|
||||
ind_str = format_inductance(
|
||||
RFTools.impedance_to_inductance(imp_b, s11_b.freq) -
|
||||
RFTools.impedance_to_inductance(imp_a, s11_a.freq))
|
||||
RFTools.impedance_to_inductance(imp_b, s11_b.freq)
|
||||
- RFTools.impedance_to_inductance(imp_a, s11_a.freq)
|
||||
)
|
||||
|
||||
imp_p_a = RFTools.serial_to_parallel(imp_a)
|
||||
imp_p_b = RFTools.serial_to_parallel(imp_b)
|
||||
imp_p = imp_p_b - imp_p_a
|
||||
|
||||
cap_p_str = format_capacitance(
|
||||
RFTools.impedance_to_capacitance(imp_p_b, s11_b.freq) -
|
||||
RFTools.impedance_to_capacitance(imp_p_a, s11_a.freq))
|
||||
RFTools.impedance_to_capacitance(imp_p_b, s11_b.freq)
|
||||
- RFTools.impedance_to_capacitance(imp_p_a, s11_a.freq)
|
||||
)
|
||||
ind_p_str = format_inductance(
|
||||
RFTools.impedance_to_inductance(imp_p_b, s11_b.freq) -
|
||||
RFTools.impedance_to_inductance(imp_p_a, s11_a.freq))
|
||||
RFTools.impedance_to_inductance(imp_p_b, s11_b.freq)
|
||||
- RFTools.impedance_to_inductance(imp_p_a, s11_a.freq)
|
||||
)
|
||||
|
||||
x_str = cap_str if imp.imag < 0 else ind_str
|
||||
x_p_str = cap_p_str if imp_p.imag < 0 else ind_p_str
|
||||
|
||||
self.label['actualfreq'].setText(
|
||||
format_frequency_space(s11_b.freq - s11_a.freq))
|
||||
self.label['lambda'].setText(
|
||||
format_wavelength(s11_b.wavelength - s11_a.wavelength))
|
||||
self.label['admittance'].setText(format_complex_adm(imp_p, True))
|
||||
self.label['impedance'].setText(format_complex_imp(imp, True))
|
||||
self.label["actualfreq"].setText(
|
||||
format_frequency_space(s11_b.freq - s11_a.freq)
|
||||
)
|
||||
self.label["lambda"].setText(
|
||||
format_wavelength(s11_b.wavelength - s11_a.wavelength)
|
||||
)
|
||||
self.label["admittance"].setText(format_complex_adm(imp_p, True))
|
||||
self.label["impedance"].setText(format_complex_imp(imp, True))
|
||||
|
||||
self.label['parc'].setText(cap_p_str)
|
||||
self.label['parl'].setText(ind_p_str)
|
||||
self.label['parlc'].setText(x_p_str)
|
||||
self.label["parc"].setText(cap_p_str)
|
||||
self.label["parl"].setText(ind_p_str)
|
||||
self.label["parlc"].setText(x_p_str)
|
||||
|
||||
self.label['parr'].setText(format_resistance(imp_p.real, True))
|
||||
self.label['returnloss'].setText(
|
||||
format_gain(s11_b.gain - s11_a.gain, self.returnloss_is_positive))
|
||||
self.label['s11groupdelay'].setText(format_group_delay(
|
||||
RFTools.groupDelay(b.s11, 1) -
|
||||
RFTools.groupDelay(a.s11, 1)))
|
||||
self.label["parr"].setText(format_resistance(imp_p.real, True))
|
||||
self.label["returnloss"].setText(
|
||||
format_gain(s11_b.gain - s11_a.gain, self.returnloss_is_positive)
|
||||
)
|
||||
self.label["s11groupdelay"].setText(
|
||||
format_group_delay(
|
||||
RFTools.groupDelay(b.s11, 1) - RFTools.groupDelay(a.s11, 1)
|
||||
)
|
||||
)
|
||||
|
||||
self.label['s11mag'].setText(
|
||||
format_magnitude(abs(s11_b.z) - abs(s11_a.z)))
|
||||
self.label['s11phase'].setText(format_phase(s11_b.phase - s11_a.phase))
|
||||
self.label['s11polar'].setText(
|
||||
self.label["s11mag"].setText(
|
||||
format_magnitude(abs(s11_b.z) - abs(s11_a.z))
|
||||
)
|
||||
self.label["s11phase"].setText(format_phase(s11_b.phase - s11_a.phase))
|
||||
self.label["s11polar"].setText(
|
||||
f"{round(abs(s11_b.z) - abs(s11_a.z), 2)}∠"
|
||||
f"{format_phase(s11_b.phase - s11_a.phase)}")
|
||||
self.label['s11q'].setText(format_q_factor(
|
||||
s11_b.qFactor() - s11_a.qFactor(), True))
|
||||
self.label['s11z'].setText(format_resistance(abs(imp)))
|
||||
self.label['serc'].setText(cap_str)
|
||||
self.label['serl'].setText(ind_str)
|
||||
self.label['serlc'].setText(x_str)
|
||||
self.label['serr'].setText(format_resistance(imp.real, True))
|
||||
self.label['vswr'].setText(format_vswr(s11_b.vswr - s11_a.vswr))
|
||||
f"{format_phase(s11_b.phase - s11_a.phase)}"
|
||||
)
|
||||
self.label["s11q"].setText(
|
||||
format_q_factor(s11_b.qFactor() - s11_a.qFactor(), True)
|
||||
)
|
||||
self.label["s11z"].setText(format_resistance(abs(imp)))
|
||||
self.label["serc"].setText(cap_str)
|
||||
self.label["serl"].setText(ind_str)
|
||||
self.label["serlc"].setText(x_str)
|
||||
self.label["serr"].setText(format_resistance(imp.real, True))
|
||||
self.label["vswr"].setText(format_vswr(s11_b.vswr - s11_a.vswr))
|
||||
|
||||
if len(a.s21) == len(a.s11):
|
||||
s21_a = a.s21[1]
|
||||
s21_b = b.s21[1]
|
||||
self.label['s21gain'].setText(format_gain(
|
||||
s21_b.gain - s21_a.gain))
|
||||
self.label['s21groupdelay'].setText(format_group_delay(
|
||||
(RFTools.groupDelay(b.s21, 1) -
|
||||
RFTools.groupDelay(a.s21, 1)) / 2))
|
||||
self.label['s21mag'].setText(format_magnitude(
|
||||
abs(s21_b.z) - abs(s21_a.z)))
|
||||
self.label['s21phase'].setText(format_phase(
|
||||
s21_b.phase - s21_a.phase))
|
||||
self.label['s21polar'].setText(
|
||||
self.label["s21gain"].setText(format_gain(s21_b.gain - s21_a.gain))
|
||||
self.label["s21groupdelay"].setText(
|
||||
format_group_delay(
|
||||
(
|
||||
RFTools.groupDelay(b.s21, 1)
|
||||
- RFTools.groupDelay(a.s21, 1)
|
||||
)
|
||||
/ 2
|
||||
)
|
||||
)
|
||||
self.label["s21mag"].setText(
|
||||
format_magnitude(abs(s21_b.z) - abs(s21_a.z))
|
||||
)
|
||||
self.label["s21phase"].setText(
|
||||
format_phase(s21_b.phase - s21_a.phase)
|
||||
)
|
||||
self.label["s21polar"].setText(
|
||||
f"{round(abs(s21_b.z) - abs(s21_a.z), 2)}∠"
|
||||
f"{format_phase(s21_b.phase - s21_a.phase)}")
|
||||
f"{format_phase(s21_b.phase - s21_a.phase)}"
|
||||
)
|
||||
|
|
|
@ -56,10 +56,10 @@ TYPES = (
|
|||
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),
|
||||
Label("s21realimagshunt", "S21 R+jX shunt", "S21 Z Real+Imag shunt", False),
|
||||
Label(
|
||||
"s21realimagseries", "S21 R+jX series", "S21 Z Real+Imag series", False
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
@ -67,31 +67,40 @@ def default_label_ids() -> str:
|
|||
return [label.label_id for label in TYPES if label.default_active]
|
||||
|
||||
|
||||
class Value():
|
||||
class Value:
|
||||
"""Contains the data area to calculate marker values from"""
|
||||
|
||||
def __init__(self, freq: int = 0,
|
||||
s11: List[Datapoint] = None,
|
||||
s21: List[Datapoint] = None):
|
||||
def __init__(
|
||||
self,
|
||||
freq: int = 0,
|
||||
s11: List[Datapoint] = None,
|
||||
s21: List[Datapoint] = None,
|
||||
):
|
||||
self.freq = freq
|
||||
self.s11 = [] if s11 is None else s11[:]
|
||||
self.s21 = [] if s21 is None else s21[:]
|
||||
|
||||
def store(self, index: int,
|
||||
s11: List[Datapoint],
|
||||
s21: List[Datapoint]):
|
||||
def store(self, index: int, s11: List[Datapoint], s21: List[Datapoint]):
|
||||
# handle boundaries
|
||||
if index == 0:
|
||||
index = 1
|
||||
s11 = [s11[0], ] + s11
|
||||
s11 = [
|
||||
s11[0],
|
||||
] + s11
|
||||
if s21:
|
||||
s21 = [s21[0], ] + s21
|
||||
s21 = [
|
||||
s21[0],
|
||||
] + s21
|
||||
if index == len(s11):
|
||||
s11 += [s11[-1], ]
|
||||
s11 += [
|
||||
s11[-1],
|
||||
]
|
||||
if s21:
|
||||
s21 += [s21[-1], ]
|
||||
s21 += [
|
||||
s21[-1],
|
||||
]
|
||||
|
||||
self.freq = s11[1].freq
|
||||
self.s11 = s11[index - 1:index + 2]
|
||||
self.s11 = s11[index - 1 : index + 2]
|
||||
if s21:
|
||||
self.s21 = s21[index - 1:index + 2]
|
||||
self.s21 = s21[index - 1 : index + 2]
|
||||
|
|
|
@ -81,7 +81,8 @@ class Marker(QtCore.QObject, Value):
|
|||
if self.qsettings:
|
||||
Marker._instances += 1
|
||||
Marker.active_labels = self.qsettings.value(
|
||||
"MarkerFields", defaultValue=default_label_ids())
|
||||
"MarkerFields", defaultValue=default_label_ids()
|
||||
)
|
||||
self.index = Marker._instances
|
||||
|
||||
if not self.name:
|
||||
|
@ -92,7 +93,9 @@ class Marker(QtCore.QObject, Value):
|
|||
self.frequencyInput.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.frequencyInput.editingFinished.connect(
|
||||
lambda: self.setFrequency(
|
||||
parse_frequency(self.frequencyInput.text())))
|
||||
parse_frequency(self.frequencyInput.text())
|
||||
)
|
||||
)
|
||||
|
||||
###############################################################
|
||||
# Data display labels
|
||||
|
@ -101,8 +104,8 @@ class Marker(QtCore.QObject, Value):
|
|||
self.label = {
|
||||
label.label_id: MarkerLabel(label.name) for label in TYPES
|
||||
}
|
||||
self.label['actualfreq'].setMinimumWidth(100)
|
||||
self.label['returnloss'].setMinimumWidth(80)
|
||||
self.label["actualfreq"].setMinimumWidth(100)
|
||||
self.label["returnloss"].setMinimumWidth(80)
|
||||
|
||||
###############################################################
|
||||
# Marker control layout
|
||||
|
@ -112,8 +115,11 @@ class Marker(QtCore.QObject, Value):
|
|||
self.btnColorPicker.setMinimumHeight(20)
|
||||
self.btnColorPicker.setFixedWidth(20)
|
||||
self.btnColorPicker.clicked.connect(
|
||||
lambda: self.setColor(QtWidgets.QColorDialog.getColor(
|
||||
self.color, options=QtWidgets.QColorDialog.ShowAlphaChannel))
|
||||
lambda: self.setColor(
|
||||
QtWidgets.QColorDialog.getColor(
|
||||
self.color, options=QtWidgets.QColorDialog.ShowAlphaChannel
|
||||
)
|
||||
)
|
||||
)
|
||||
self.isMouseControlledRadioButton = QtWidgets.QRadioButton()
|
||||
|
||||
|
@ -133,7 +139,9 @@ class Marker(QtCore.QObject, Value):
|
|||
try:
|
||||
self.setColor(
|
||||
self.qsettings.value(
|
||||
f"Marker{self.count()}Color", COLORS[self.count()]))
|
||||
f"Marker{self.count()}Color", COLORS[self.count()]
|
||||
)
|
||||
)
|
||||
except AttributeError: # happens when qsettings == None
|
||||
self.setColor(COLORS[1])
|
||||
except IndexError:
|
||||
|
@ -159,8 +167,7 @@ class Marker(QtCore.QObject, Value):
|
|||
|
||||
def _add_active_labels(self, label_id, form):
|
||||
if label_id in self.label:
|
||||
form.addRow(
|
||||
f"{self.label[label_id].name}:", self.label[label_id])
|
||||
form.addRow(f"{self.label[label_id].name}:", self.label[label_id])
|
||||
self.label[label_id].show()
|
||||
|
||||
def _size_str(self) -> str:
|
||||
|
@ -171,9 +178,9 @@ class Marker(QtCore.QObject, Value):
|
|||
|
||||
def setScale(self, scale):
|
||||
self.group_box.setMaximumWidth(int(340 * scale))
|
||||
self.label['actualfreq'].setMinimumWidth(int(100 * scale))
|
||||
self.label['actualfreq'].setMinimumWidth(int(100 * scale))
|
||||
self.label['returnloss'].setMinimumWidth(int(80 * scale))
|
||||
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
|
||||
self.label["actualfreq"].setMinimumWidth(int(100 * scale))
|
||||
self.label["returnloss"].setMinimumWidth(int(80 * scale))
|
||||
if self.coloredText:
|
||||
color_string = QtCore.QVariant(self.color)
|
||||
color_string.convert(QtCore.QVariant.String)
|
||||
|
@ -259,8 +266,10 @@ class Marker(QtCore.QObject, Value):
|
|||
upper_stepsize = data[-1].freq - data[-2].freq
|
||||
|
||||
# We are outside the bounds of the data, so we can't put in a marker
|
||||
if (self.freq + lower_stepsize / 2 < min_freq or
|
||||
self.freq - upper_stepsize / 2 > max_freq):
|
||||
if (
|
||||
self.freq + lower_stepsize / 2 < min_freq
|
||||
or self.freq - upper_stepsize / 2 > max_freq
|
||||
):
|
||||
return
|
||||
|
||||
min_distance = max_freq
|
||||
|
@ -286,15 +295,16 @@ class Marker(QtCore.QObject, Value):
|
|||
for v in self.label.values():
|
||||
v.setText("")
|
||||
|
||||
def updateLabels(self,
|
||||
s11: List[RFTools.Datapoint],
|
||||
s21: List[RFTools.Datapoint]):
|
||||
def updateLabels(
|
||||
self, s11: List[RFTools.Datapoint], s21: List[RFTools.Datapoint]
|
||||
):
|
||||
if not s11:
|
||||
return
|
||||
if self.location == -1: # initial position
|
||||
try:
|
||||
location = (self.index - 1) / (
|
||||
(self._instances - 1) * (len(s11) - 1))
|
||||
(self._instances - 1) * (len(s11) - 1)
|
||||
)
|
||||
self.location = int(location)
|
||||
except ZeroDivisionError:
|
||||
self.location = 0
|
||||
|
@ -309,63 +319,72 @@ class Marker(QtCore.QObject, Value):
|
|||
|
||||
imp = _s11.impedance()
|
||||
cap_str = format_capacitance(
|
||||
RFTools.impedance_to_capacitance(imp, _s11.freq))
|
||||
RFTools.impedance_to_capacitance(imp, _s11.freq)
|
||||
)
|
||||
ind_str = format_inductance(
|
||||
RFTools.impedance_to_inductance(imp, _s11.freq))
|
||||
RFTools.impedance_to_inductance(imp, _s11.freq)
|
||||
)
|
||||
|
||||
imp_p = RFTools.serial_to_parallel(imp)
|
||||
cap_p_str = format_capacitance(
|
||||
RFTools.impedance_to_capacitance(imp_p, _s11.freq))
|
||||
RFTools.impedance_to_capacitance(imp_p, _s11.freq)
|
||||
)
|
||||
ind_p_str = format_inductance(
|
||||
RFTools.impedance_to_inductance(imp_p, _s11.freq))
|
||||
RFTools.impedance_to_inductance(imp_p, _s11.freq)
|
||||
)
|
||||
|
||||
x_str = cap_str if imp.imag < 0 else ind_str
|
||||
x_p_str = cap_p_str if imp_p.imag < 0 else ind_p_str
|
||||
|
||||
self.label['actualfreq'].setText(format_frequency_space(_s11.freq))
|
||||
self.label['lambda'].setText(format_wavelength(_s11.wavelength))
|
||||
self.label['admittance'].setText(format_complex_adm(imp))
|
||||
self.label['impedance'].setText(format_complex_imp(imp))
|
||||
self.label['parc'].setText(cap_p_str)
|
||||
self.label['parl'].setText(ind_p_str)
|
||||
self.label['parlc'].setText(x_p_str)
|
||||
self.label['parr'].setText(format_resistance(imp_p.real))
|
||||
self.label['returnloss'].setText(
|
||||
format_gain(_s11.gain, self.returnloss_is_positive))
|
||||
self.label['s11groupdelay'].setText(
|
||||
format_group_delay(RFTools.groupDelay(s11, self.location)))
|
||||
self.label['s11mag'].setText(format_magnitude(abs(_s11.z)))
|
||||
self.label['s11phase'].setText(format_phase(_s11.phase))
|
||||
self.label['s11polar'].setText(
|
||||
f'{str(round(abs(_s11.z), 2))}∠{format_phase(_s11.phase)}'
|
||||
self.label["actualfreq"].setText(format_frequency_space(_s11.freq))
|
||||
self.label["lambda"].setText(format_wavelength(_s11.wavelength))
|
||||
self.label["admittance"].setText(format_complex_adm(imp))
|
||||
self.label["impedance"].setText(format_complex_imp(imp))
|
||||
self.label["parc"].setText(cap_p_str)
|
||||
self.label["parl"].setText(ind_p_str)
|
||||
self.label["parlc"].setText(x_p_str)
|
||||
self.label["parr"].setText(format_resistance(imp_p.real))
|
||||
self.label["returnloss"].setText(
|
||||
format_gain(_s11.gain, self.returnloss_is_positive)
|
||||
)
|
||||
self.label["s11groupdelay"].setText(
|
||||
format_group_delay(RFTools.groupDelay(s11, self.location))
|
||||
)
|
||||
self.label["s11mag"].setText(format_magnitude(abs(_s11.z)))
|
||||
self.label["s11phase"].setText(format_phase(_s11.phase))
|
||||
self.label["s11polar"].setText(
|
||||
f"{str(round(abs(_s11.z), 2))}∠{format_phase(_s11.phase)}"
|
||||
)
|
||||
|
||||
self.label['s11q'].setText(format_q_factor(_s11.qFactor()))
|
||||
self.label['s11z'].setText(format_resistance(abs(imp)))
|
||||
self.label['serc'].setText(cap_str)
|
||||
self.label['serl'].setText(ind_str)
|
||||
self.label['serlc'].setText(x_str)
|
||||
self.label['serr'].setText(format_resistance(imp.real))
|
||||
self.label['vswr'].setText(format_vswr(_s11.vswr))
|
||||
self.label["s11q"].setText(format_q_factor(_s11.qFactor()))
|
||||
self.label["s11z"].setText(format_resistance(abs(imp)))
|
||||
self.label["serc"].setText(cap_str)
|
||||
self.label["serl"].setText(ind_str)
|
||||
self.label["serlc"].setText(x_str)
|
||||
self.label["serr"].setText(format_resistance(imp.real))
|
||||
self.label["vswr"].setText(format_vswr(_s11.vswr))
|
||||
|
||||
if len(s21) == len(s11):
|
||||
_s21 = s21[self.location]
|
||||
self.label['s21gain'].setText(format_gain(_s21.gain))
|
||||
self.label['s21groupdelay'].setText(
|
||||
format_group_delay(RFTools.groupDelay(s21, self.location) / 2))
|
||||
self.label['s21mag'].setText(format_magnitude(abs(_s21.z)))
|
||||
self.label['s21phase'].setText(format_phase(_s21.phase))
|
||||
self.label['s21polar'].setText(
|
||||
f'{str(round(abs(_s21.z), 2))}∠{format_phase(_s21.phase)}'
|
||||
self.label["s21gain"].setText(format_gain(_s21.gain))
|
||||
self.label["s21groupdelay"].setText(
|
||||
format_group_delay(RFTools.groupDelay(s21, self.location) / 2)
|
||||
)
|
||||
self.label["s21mag"].setText(format_magnitude(abs(_s21.z)))
|
||||
self.label["s21phase"].setText(format_phase(_s21.phase))
|
||||
self.label["s21polar"].setText(
|
||||
f"{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))
|
||||
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)
|
||||
)
|
||||
|
|
|
@ -26,9 +26,14 @@ from PyQt5 import QtWidgets, QtCore, QtGui
|
|||
|
||||
from NanoVNASaver import Defaults
|
||||
from .Windows import (
|
||||
AboutWindow, AnalysisWindow, CalibrationWindow,
|
||||
DeviceSettingsWindow, DisplaySettingsWindow, SweepSettingsWindow,
|
||||
TDRWindow, FilesWindow
|
||||
AboutWindow,
|
||||
AnalysisWindow,
|
||||
CalibrationWindow,
|
||||
DeviceSettingsWindow,
|
||||
DisplaySettingsWindow,
|
||||
SweepSettingsWindow,
|
||||
TDRWindow,
|
||||
FilesWindow,
|
||||
)
|
||||
from .Controls.MarkerControl import MarkerControl
|
||||
from .Controls.SweepControl import SweepControl
|
||||
|
@ -40,14 +45,26 @@ from .RFTools import corr_att_data
|
|||
from .Charts.Chart import Chart
|
||||
from .Charts import (
|
||||
CapacitanceChart,
|
||||
CombinedLogMagChart, GroupDelayChart, InductanceChart,
|
||||
LogMagChart, PhaseChart,
|
||||
MagnitudeChart, MagnitudeZChart, MagnitudeZShuntChart,
|
||||
CombinedLogMagChart,
|
||||
GroupDelayChart,
|
||||
InductanceChart,
|
||||
LogMagChart,
|
||||
PhaseChart,
|
||||
MagnitudeChart,
|
||||
MagnitudeZChart,
|
||||
MagnitudeZShuntChart,
|
||||
MagnitudeZSeriesChart,
|
||||
QualityFactorChart, VSWRChart, PermeabilityChart, PolarChart,
|
||||
QualityFactorChart,
|
||||
VSWRChart,
|
||||
PermeabilityChart,
|
||||
PolarChart,
|
||||
RealImaginaryMuChart,
|
||||
RealImaginaryZChart, RealImaginaryZShuntChart, RealImaginaryZSeriesChart,
|
||||
SmithChart, SParameterChart, TDRChart,
|
||||
RealImaginaryZChart,
|
||||
RealImaginaryZShuntChart,
|
||||
RealImaginaryZSeriesChart,
|
||||
SmithChart,
|
||||
SParameterChart,
|
||||
TDRChart,
|
||||
)
|
||||
from .Calibration import Calibration
|
||||
from .Marker.Widget import Marker
|
||||
|
@ -69,10 +86,11 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
def __init__(self):
|
||||
super().__init__()
|
||||
self.s21att = 0.0
|
||||
if getattr(sys, 'frozen', False):
|
||||
if getattr(sys, "frozen", False):
|
||||
logger.debug("Running from pyinstaller bundle")
|
||||
self.icon = QtGui.QIcon(
|
||||
f"{sys._MEIPASS}/icon_48x48.png") # pylint: disable=no-member
|
||||
f"{sys._MEIPASS}/icon_48x48.png"
|
||||
) # pylint: disable=no-member
|
||||
else:
|
||||
self.icon = QtGui.QIcon("icon_48x48.png")
|
||||
self.setWindowIcon(self.icon)
|
||||
|
@ -80,7 +98,8 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
QtCore.QSettings.IniFormat,
|
||||
QtCore.QSettings.UserScope,
|
||||
"NanoVNASaver",
|
||||
"NanoVNASaver")
|
||||
"NanoVNASaver",
|
||||
)
|
||||
logger.info("Settings from: %s", self.settings.fileName())
|
||||
Defaults.cfg = Defaults.restore(self.settings)
|
||||
self.threadpool = QtCore.QThreadPool()
|
||||
|
@ -128,13 +147,17 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
outer.addWidget(scrollarea)
|
||||
self.setLayout(outer)
|
||||
scrollarea.setWidgetResizable(True)
|
||||
self.resize(Defaults.cfg.gui.window_width,
|
||||
Defaults.cfg.gui.window_height)
|
||||
self.resize(
|
||||
Defaults.cfg.gui.window_width, Defaults.cfg.gui.window_height
|
||||
)
|
||||
scrollarea.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
)
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
)
|
||||
widget = QtWidgets.QWidget()
|
||||
widget.setLayout(layout)
|
||||
scrollarea.setWidget(widget)
|
||||
|
@ -149,25 +172,30 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
"magnitude_z": MagnitudeZChart("S11 |Z|"),
|
||||
"permeability": PermeabilityChart(
|
||||
"S11 R/\N{GREEK SMALL LETTER OMEGA} &"
|
||||
" X/\N{GREEK SMALL LETTER OMEGA}"),
|
||||
" X/\N{GREEK SMALL LETTER OMEGA}"
|
||||
),
|
||||
"phase": PhaseChart("S11 Phase"),
|
||||
"q_factor": QualityFactorChart("S11 Quality Factor"),
|
||||
"real_imag": RealImaginaryZChart("S11 R+jX"),
|
||||
"real_imag_mu": RealImaginaryMuChart("S11 \N{GREEK SMALL LETTER MU}"),
|
||||
"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"),
|
||||
},
|
||||
"s21": {
|
||||
"group_delay": GroupDelayChart("S21 Group Delay",
|
||||
reflective=False),
|
||||
"group_delay": GroupDelayChart(
|
||||
"S21 Group Delay", 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": RealImaginaryZShuntChart("S21 R+jX shunt"),
|
||||
"real_imag_series": RealImaginaryZSeriesChart(
|
||||
"S21 R+jX series"),
|
||||
"S21 R+jX series"
|
||||
),
|
||||
"phase": PhaseChart("S21 Phase"),
|
||||
"polar": PolarChart("S21 Polar Plot"),
|
||||
"s_parameter": SParameterChart("S21 Real/Imaginary"),
|
||||
|
@ -190,8 +218,13 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
|
||||
# List of all charts that can be selected for display
|
||||
self.selectable_charts = (
|
||||
self.s11charts + self.s21charts +
|
||||
self.combinedCharts + [self.tdr_mainwindow_chart, ])
|
||||
self.s11charts
|
||||
+ self.s21charts
|
||||
+ self.combinedCharts
|
||||
+ [
|
||||
self.tdr_mainwindow_chart,
|
||||
]
|
||||
)
|
||||
|
||||
# List of all charts that subscribe to updates (including duplicates!)
|
||||
self.subscribing_charts = []
|
||||
|
@ -314,7 +347,8 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
btn_show_analysis = QtWidgets.QPushButton("Analysis ...")
|
||||
btn_show_analysis.setMinimumHeight(20)
|
||||
btn_show_analysis.clicked.connect(
|
||||
lambda: self.display_window("analysis"))
|
||||
lambda: self.display_window("analysis")
|
||||
)
|
||||
self.marker_column.addWidget(btn_show_analysis)
|
||||
|
||||
###############################################################
|
||||
|
@ -335,10 +369,10 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.tdr_result_label = QtWidgets.QLabel()
|
||||
self.tdr_result_label.setMinimumHeight(20)
|
||||
tdr_control_layout.addRow(
|
||||
"Estimated cable length:", self.tdr_result_label)
|
||||
"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.setMinimumHeight(20)
|
||||
self.tdr_button.clicked.connect(lambda: self.display_window("tdr"))
|
||||
|
||||
|
@ -351,8 +385,13 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
###############################################################
|
||||
|
||||
left_column.addSpacerItem(
|
||||
QtWidgets.QSpacerItem(1, 1, QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.Expanding))
|
||||
QtWidgets.QSpacerItem(
|
||||
1,
|
||||
1,
|
||||
QtWidgets.QSizePolicy.Fixed,
|
||||
QtWidgets.QSizePolicy.Expanding,
|
||||
)
|
||||
)
|
||||
|
||||
###############################################################
|
||||
# Reference control
|
||||
|
@ -390,7 +429,8 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
btnOpenCalibrationWindow.setMinimumHeight(20)
|
||||
self.calibrationWindow = CalibrationWindow(self)
|
||||
btnOpenCalibrationWindow.clicked.connect(
|
||||
lambda: self.display_window("calibration"))
|
||||
lambda: self.display_window("calibration")
|
||||
)
|
||||
|
||||
###############################################################
|
||||
# Display setup
|
||||
|
@ -399,22 +439,21 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
btn_display_setup = QtWidgets.QPushButton("Display setup ...")
|
||||
btn_display_setup.setMinimumHeight(20)
|
||||
btn_display_setup.setMaximumWidth(240)
|
||||
btn_display_setup.clicked.connect(
|
||||
lambda: self.display_window("setup"))
|
||||
btn_display_setup.clicked.connect(lambda: self.display_window("setup"))
|
||||
|
||||
btn_about = QtWidgets.QPushButton("About ...")
|
||||
btn_about.setMinimumHeight(20)
|
||||
btn_about.setMaximumWidth(240)
|
||||
|
||||
btn_about.clicked.connect(
|
||||
lambda: self.display_window("about"))
|
||||
btn_about.clicked.connect(lambda: self.display_window("about"))
|
||||
|
||||
btn_open_file_window = QtWidgets.QPushButton("Files")
|
||||
btn_open_file_window.setMinimumHeight(20)
|
||||
btn_open_file_window.setMaximumWidth(240)
|
||||
|
||||
btn_open_file_window.clicked.connect(
|
||||
lambda: self.display_window("file"))
|
||||
lambda: self.display_window("file")
|
||||
)
|
||||
|
||||
button_grid = QtWidgets.QGridLayout()
|
||||
button_grid.addWidget(btn_open_file_window, 0, 0)
|
||||
|
@ -484,8 +523,7 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
m2 = Marker("Reference")
|
||||
m2.location = self.markers[0].location
|
||||
m2.resetLabels()
|
||||
m2.updateLabels(self.ref_data.s11,
|
||||
self.ref_data.s21)
|
||||
m2.updateLabels(self.ref_data.s11, self.ref_data.s21)
|
||||
else:
|
||||
logger.warning("No reference data for marker")
|
||||
|
||||
|
@ -525,7 +563,8 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
min_vswr = min(s11, key=lambda data: data.vswr)
|
||||
self.s11_min_swr_label.setText(
|
||||
f"{format_vswr(min_vswr.vswr)} @"
|
||||
f" {format_frequency(min_vswr.freq)}")
|
||||
f" {format_frequency(min_vswr.freq)}"
|
||||
)
|
||||
self.s11_min_rl_label.setText(format_gain(min_vswr.gain))
|
||||
else:
|
||||
self.s11_min_swr_label.setText("")
|
||||
|
@ -536,10 +575,12 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
max_gain = max(s21, key=lambda data: data.gain)
|
||||
self.s21_min_gain_label.setText(
|
||||
f"{format_gain(min_gain.gain)}"
|
||||
f" @ {format_frequency(min_gain.freq)}")
|
||||
f" @ {format_frequency(min_gain.freq)}"
|
||||
)
|
||||
self.s21_max_gain_label.setText(
|
||||
f"{format_gain(max_gain.gain)}"
|
||||
f" @ {format_frequency(max_gain.freq)}")
|
||||
f" @ {format_frequency(max_gain.freq)}"
|
||||
)
|
||||
else:
|
||||
self.s21_min_gain_label.setText("")
|
||||
self.s21_max_gain_label.setText("")
|
||||
|
@ -551,8 +592,7 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self._sweep_control(start=False)
|
||||
|
||||
for marker in self.markers:
|
||||
marker.frequencyInput.textEdited.emit(
|
||||
marker.frequencyInput.text())
|
||||
marker.frequencyInput.textEdited.emit(marker.frequencyInput.text())
|
||||
|
||||
def setReference(self, s11=None, s21=None, source=None):
|
||||
if not s11:
|
||||
|
@ -581,11 +621,13 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
if self.sweepSource != "":
|
||||
insert += (
|
||||
f"Sweep: {self.sweepSource} @ {len(self.data.s11)} points"
|
||||
f"{', ' if self.referenceSource else ''}")
|
||||
f"{', ' if self.referenceSource else ''}"
|
||||
)
|
||||
if self.referenceSource != "":
|
||||
insert += (
|
||||
f"Reference: {self.referenceSource} @"
|
||||
f" {len(self.ref_data.s11)} points")
|
||||
f" {len(self.ref_data.s11)} points"
|
||||
)
|
||||
insert += ")"
|
||||
title = f"{self.baseTitle} {insert or ''}"
|
||||
self.setWindowTitle(title)
|
||||
|
@ -612,7 +654,7 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
self.showError(self.worker.error_message)
|
||||
with contextlib.suppress(IOError):
|
||||
self.vna.flushSerialBuffers() # Remove any left-over data
|
||||
self.vna.reconnect() # try reconnection
|
||||
self.vna.reconnect() # try reconnection
|
||||
self.sweepFinished()
|
||||
|
||||
def popoutChart(self, chart: Chart):
|
||||
|
@ -661,8 +703,12 @@ class NanoVNASaver(QtWidgets.QWidget):
|
|||
new_width = qf_new.horizontalAdvance(standard_string)
|
||||
old_width = qf_normal.horizontalAdvance(standard_string)
|
||||
self.scaleFactor = new_width / old_width
|
||||
logger.debug("New font width: %f, normal font: %f, factor: %f",
|
||||
new_width, old_width, self.scaleFactor)
|
||||
logger.debug(
|
||||
"New font width: %f, normal font: %f, factor: %f",
|
||||
new_width,
|
||||
old_width,
|
||||
self.scaleFactor,
|
||||
)
|
||||
# TODO: Update all the fixed widths to account for the scaling
|
||||
for m in self.markers:
|
||||
m.get_data_layout().setFont(font)
|
||||
|
|
|
@ -34,12 +34,12 @@ class Datapoint(NamedTuple):
|
|||
|
||||
@property
|
||||
def z(self) -> complex:
|
||||
""" return the s value complex number """
|
||||
"""return the s value complex number"""
|
||||
return complex(self.re, self.im)
|
||||
|
||||
@property
|
||||
def phase(self) -> float:
|
||||
""" return the datapoint's phase value """
|
||||
"""return the datapoint's phase value"""
|
||||
return cmath.phase(self.z)
|
||||
|
||||
@property
|
||||
|
@ -77,11 +77,11 @@ class Datapoint(NamedTuple):
|
|||
|
||||
def capacitiveEquivalent(self, ref_impedance: float = 50) -> float:
|
||||
return impedance_to_capacitance(
|
||||
self.impedance(ref_impedance), self.freq)
|
||||
self.impedance(ref_impedance), self.freq
|
||||
)
|
||||
|
||||
def inductiveEquivalent(self, ref_impedance: float = 50) -> float:
|
||||
return impedance_to_inductance(
|
||||
self.impedance(ref_impedance), self.freq)
|
||||
return impedance_to_inductance(self.impedance(ref_impedance), self.freq)
|
||||
|
||||
|
||||
def gamma_to_impedance(gamma: complex, ref_impedance: float = 50) -> complex:
|
||||
|
@ -124,9 +124,10 @@ def norm_to_impedance(z: complex, ref_impedance: float = 50) -> complex:
|
|||
|
||||
def parallel_to_serial(z: complex) -> complex:
|
||||
"""Convert parallel impedance to serial impedance equivalent"""
|
||||
z_sq_sum = z.real ** 2 + z.imag ** 2 or 10.0e-30
|
||||
return complex(z.real * z.imag ** 2 / z_sq_sum,
|
||||
z.real ** 2 * z.imag / z_sq_sum)
|
||||
z_sq_sum = z.real**2 + z.imag**2 or 10.0e-30
|
||||
return complex(
|
||||
z.real * z.imag**2 / z_sq_sum, z.real**2 * z.imag / z_sq_sum
|
||||
)
|
||||
|
||||
|
||||
def reflection_coefficient(z: complex, ref_impedance: float = 50) -> complex:
|
||||
|
@ -136,7 +137,7 @@ def reflection_coefficient(z: complex, ref_impedance: float = 50) -> complex:
|
|||
|
||||
def serial_to_parallel(z: complex) -> complex:
|
||||
"""Convert serial impedance to parallel impedance equivalent"""
|
||||
z_sq_sum = z.real ** 2 + z.imag ** 2
|
||||
z_sq_sum = z.real**2 + z.imag**2
|
||||
if z.real == 0 and z.imag == 0:
|
||||
return complex(math.inf, math.inf)
|
||||
if z.imag == 0:
|
||||
|
@ -150,7 +151,7 @@ def corr_att_data(data: List[Datapoint], att: float) -> List[Datapoint]:
|
|||
"""Correct the ratio for a given attenuation on s21 input"""
|
||||
if att <= 0:
|
||||
return data
|
||||
att = 10**(att / 20)
|
||||
att = 10 ** (att / 20)
|
||||
ndata = []
|
||||
for dp in data:
|
||||
corrected = dp.z * att
|
||||
|
|
|
@ -22,8 +22,29 @@ from decimal import Context, Decimal, InvalidOperation
|
|||
from typing import NamedTuple
|
||||
from numbers import Number, Real
|
||||
|
||||
PREFIXES = ("q", "r", "y", "z", "a", "f", "p", "n", "µ", "m",
|
||||
"", "k", "M", "G", "T", "P", "E", "Z", "Y", "R", "Q")
|
||||
PREFIXES = (
|
||||
"q",
|
||||
"r",
|
||||
"y",
|
||||
"z",
|
||||
"a",
|
||||
"f",
|
||||
"p",
|
||||
"n",
|
||||
"µ",
|
||||
"m",
|
||||
"",
|
||||
"k",
|
||||
"M",
|
||||
"G",
|
||||
"T",
|
||||
"P",
|
||||
"E",
|
||||
"Z",
|
||||
"Y",
|
||||
"R",
|
||||
"Q",
|
||||
)
|
||||
|
||||
|
||||
def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
|
||||
|
@ -32,17 +53,17 @@ def clamp_value(value: Real, rmin: Real, rmax: Real) -> Real:
|
|||
|
||||
|
||||
def round_ceil(value: Real, digits: int = 0) -> Real:
|
||||
factor = 10 ** -digits
|
||||
factor = 10**-digits
|
||||
return factor * math.ceil(value / factor)
|
||||
|
||||
|
||||
def round_floor(value: Real, digits: int = 0) -> Real:
|
||||
factor = 10 ** -digits
|
||||
factor = 10**-digits
|
||||
return factor * math.floor(value / factor)
|
||||
|
||||
|
||||
def log_floor_125(x: float) -> float:
|
||||
log_base = 10**(math.floor(math.log10(x)))
|
||||
log_base = 10 ** (math.floor(math.log10(x)))
|
||||
log_factor = x / log_base
|
||||
if log_factor >= 5:
|
||||
return 5 * log_base
|
||||
|
@ -80,31 +101,44 @@ class Value:
|
|||
self.fmt = fmt
|
||||
if isinstance(value, str):
|
||||
self._value = Decimal(math.nan)
|
||||
if value.lower() != 'nan':
|
||||
if value.lower() != "nan":
|
||||
self.parse(value)
|
||||
else:
|
||||
self._value = Decimal(value, context=Value.CTX)
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return (f"{self.__class__.__name__}("
|
||||
f"{repr(self._value)}, '{self._unit}', {self.fmt})")
|
||||
return (
|
||||
f"{self.__class__.__name__}("
|
||||
f"{repr(self._value)}, '{self._unit}', {self.fmt})"
|
||||
)
|
||||
|
||||
def __str__(self) -> str:
|
||||
fmt = self.fmt
|
||||
if math.isnan(self._value):
|
||||
return f"-{fmt.space_str}{self._unit}"
|
||||
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)
|
||||
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
|
||||
)
|
||||
if self._value < fmt.printable_min:
|
||||
return fmt.unprintable_under + self._unit
|
||||
if self._value > fmt.printable_max:
|
||||
return fmt.unprintable_over + self._unit
|
||||
|
||||
offset = clamp_value(
|
||||
int(math.log10(abs(self._value)) // 3),
|
||||
fmt.min_offset, fmt.max_offset) if self._value else 0
|
||||
offset = (
|
||||
clamp_value(
|
||||
int(math.log10(abs(self._value)) // 3),
|
||||
fmt.min_offset,
|
||||
fmt.max_offset,
|
||||
)
|
||||
if self._value
|
||||
else 0
|
||||
)
|
||||
|
||||
real = float(self._value) / (10 ** (offset * 3))
|
||||
|
||||
|
@ -112,8 +146,9 @@ class Value:
|
|||
formstr = ".0f"
|
||||
else:
|
||||
max_digits = fmt.max_nr_digits + (
|
||||
(1 if not fmt.fix_decimals and abs(real) < 10 else 0) +
|
||||
(1 if not fmt.fix_decimals and abs(real) < 100 else 0))
|
||||
(1 if not fmt.fix_decimals and abs(real) < 10 else 0)
|
||||
+ (1 if not fmt.fix_decimals and abs(real) < 100 else 0)
|
||||
)
|
||||
formstr = f".{max_digits - 3}f"
|
||||
|
||||
if self.fmt.allways_signed:
|
||||
|
@ -150,10 +185,13 @@ class Value:
|
|||
value = value.replace(" ", "") # Ignore spaces
|
||||
|
||||
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.endswith(self._unit)
|
||||
or (
|
||||
self.fmt.parse_sloppy_unit
|
||||
and value.lower().endswith(self._unit.lower())
|
||||
)
|
||||
): # strip unit
|
||||
value = value[: -len(self._unit)]
|
||||
|
||||
factor = 1
|
||||
# fix for e.g. KHz, mHz gHz as milli-Hertz mostly makes no
|
||||
|
@ -170,13 +208,14 @@ class Value:
|
|||
self._value = -math.inf
|
||||
else:
|
||||
try:
|
||||
self._value = (Decimal(value, context=Value.CTX)
|
||||
* Decimal(factor, context=Value.CTX))
|
||||
self._value = Decimal(value, context=Value.CTX) * Decimal(
|
||||
factor, context=Value.CTX
|
||||
)
|
||||
except InvalidOperation as exc:
|
||||
raise ValueError() from exc
|
||||
self._value = clamp_value(self._value,
|
||||
self.fmt.parse_clamp_min,
|
||||
self.fmt.parse_clamp_max)
|
||||
self._value = clamp_value(
|
||||
self._value, self.fmt.parse_clamp_min, self.fmt.parse_clamp_max
|
||||
)
|
||||
return self
|
||||
|
||||
@property
|
||||
|
|
|
@ -57,9 +57,12 @@ class BandsModel(QtCore.QAbstractTableModel):
|
|||
# These bands correspond broadly to the Danish Amateur Radio allocation
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.settings = QtCore.QSettings(QtCore.QSettings.IniFormat,
|
||||
QtCore.QSettings.UserScope,
|
||||
"NanoVNASaver", "Bands")
|
||||
self.settings = QtCore.QSettings(
|
||||
QtCore.QSettings.IniFormat,
|
||||
QtCore.QSettings.UserScope,
|
||||
"NanoVNASaver",
|
||||
"Bands",
|
||||
)
|
||||
self.settings.setIniCodec("UTF-8")
|
||||
|
||||
self.enabled = self.settings.value("ShowBands", False, bool)
|
||||
|
@ -71,7 +74,8 @@ class BandsModel(QtCore.QAbstractTableModel):
|
|||
def saveSettings(self):
|
||||
self.settings.setValue(
|
||||
"bands",
|
||||
[f"{name};{start};{end}" for name, start, end in self.bands])
|
||||
[f"{name};{start};{end}" for name, start, end in self.bands],
|
||||
)
|
||||
self.settings.sync()
|
||||
|
||||
def resetBands(self):
|
||||
|
@ -87,18 +91,22 @@ class BandsModel(QtCore.QAbstractTableModel):
|
|||
|
||||
def data(self, index: QModelIndex, role: int = ...) -> QtCore.QVariant:
|
||||
if role in [
|
||||
QtCore.Qt.DisplayRole, QtCore.Qt.ItemDataRole, QtCore.Qt.EditRole,
|
||||
QtCore.Qt.DisplayRole,
|
||||
QtCore.Qt.ItemDataRole,
|
||||
QtCore.Qt.EditRole,
|
||||
]:
|
||||
return QtCore.QVariant(self.bands[index.row()][index.column()])
|
||||
if role == QtCore.Qt.TextAlignmentRole:
|
||||
if index.column() == 0:
|
||||
return QtCore.QVariant(QtCore.Qt.AlignCenter)
|
||||
return QtCore.QVariant(
|
||||
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter)
|
||||
QtCore.Qt.AlignRight | QtCore.Qt.AlignVCenter
|
||||
)
|
||||
return QtCore.QVariant()
|
||||
|
||||
def setData(self, index: QModelIndex,
|
||||
value: typing.Any, role: int = ...) -> bool:
|
||||
def setData(
|
||||
self, index: QModelIndex, value: typing.Any, role: int = ...
|
||||
) -> bool:
|
||||
if role == QtCore.Qt.EditRole and index.isValid():
|
||||
t = self.bands[index.row()]
|
||||
name = t[0]
|
||||
|
@ -116,14 +124,14 @@ class BandsModel(QtCore.QAbstractTableModel):
|
|||
return True
|
||||
return False
|
||||
|
||||
def index(self, row: int,
|
||||
column: int, _: QModelIndex = ...) -> QModelIndex:
|
||||
def index(self, row: int, column: int, _: QModelIndex = ...) -> QModelIndex:
|
||||
return self.createIndex(row, column)
|
||||
|
||||
def addRow(self):
|
||||
self.bands.append(("New", 0, 0))
|
||||
self.dataChanged.emit(self.index(len(self.bands), 0),
|
||||
self.index(len(self.bands), 2))
|
||||
self.dataChanged.emit(
|
||||
self.index(len(self.bands), 0), self.index(len(self.bands), 2)
|
||||
)
|
||||
self.layoutChanged.emit()
|
||||
|
||||
def removeRow(self, row: int, _: QModelIndex = ...) -> bool:
|
||||
|
@ -132,10 +140,13 @@ class BandsModel(QtCore.QAbstractTableModel):
|
|||
self.saveSettings()
|
||||
return True
|
||||
|
||||
def headerData(self, section: int,
|
||||
orientation: QtCore.Qt.Orientation, role: int = ...):
|
||||
if (role == QtCore.Qt.DisplayRole and
|
||||
orientation == QtCore.Qt.Horizontal):
|
||||
def headerData(
|
||||
self, section: int, orientation: QtCore.Qt.Orientation, role: int = ...
|
||||
):
|
||||
if (
|
||||
role == QtCore.Qt.DisplayRole
|
||||
and orientation == QtCore.Qt.Horizontal
|
||||
):
|
||||
with contextlib.suppress(IndexError):
|
||||
return _HEADER_DATA[section]
|
||||
return None
|
||||
|
@ -143,9 +154,10 @@ class BandsModel(QtCore.QAbstractTableModel):
|
|||
def flags(self, index: QModelIndex) -> QtCore.Qt.ItemFlags:
|
||||
if index.isValid():
|
||||
return QtCore.Qt.ItemFlags(
|
||||
QtCore.Qt.ItemIsEditable |
|
||||
QtCore.Qt.ItemIsEnabled |
|
||||
QtCore.Qt.ItemIsSelectable)
|
||||
QtCore.Qt.ItemIsEditable
|
||||
| QtCore.Qt.ItemIsEnabled
|
||||
| QtCore.Qt.ItemIsSelectable
|
||||
)
|
||||
super().flags(index)
|
||||
|
||||
def setColor(self, color):
|
||||
|
|
|
@ -32,10 +32,13 @@ class SweepMode(Enum):
|
|||
|
||||
|
||||
class Properties:
|
||||
def __init__(self, name: str = "",
|
||||
mode: 'SweepMode' = SweepMode.SINGLE,
|
||||
averages: Tuple[int, int] = (3, 0),
|
||||
logarithmic: bool = False):
|
||||
def __init__(
|
||||
self,
|
||||
name: str = "",
|
||||
mode: "SweepMode" = SweepMode.SINGLE,
|
||||
averages: Tuple[int, int] = (3, 0),
|
||||
logarithmic: bool = False,
|
||||
):
|
||||
self.name = name
|
||||
self.mode = mode
|
||||
self.averages = averages
|
||||
|
@ -44,13 +47,19 @@ class Properties:
|
|||
def __repr__(self):
|
||||
return (
|
||||
f"Properties('{self.name}', {self.mode}, {self.averages},"
|
||||
f" {self.logarithmic})")
|
||||
f" {self.logarithmic})"
|
||||
)
|
||||
|
||||
|
||||
class Sweep:
|
||||
def __init__(self, start: int = 3600000, end: int = 30000000,
|
||||
points: int = 101, segments: int = 1,
|
||||
properties: 'Properties' = Properties()):
|
||||
def __init__(
|
||||
self,
|
||||
start: int = 3600000,
|
||||
end: int = 30000000,
|
||||
points: int = 101,
|
||||
segments: int = 1,
|
||||
properties: "Properties" = Properties(),
|
||||
):
|
||||
self.start = start
|
||||
self.end = end
|
||||
self.points = points
|
||||
|
@ -63,18 +72,22 @@ class Sweep:
|
|||
def __repr__(self) -> str:
|
||||
return (
|
||||
f"Sweep({self.start}, {self.end}, {self.points}, {self.segments},"
|
||||
f" {self.properties})")
|
||||
f" {self.properties})"
|
||||
)
|
||||
|
||||
def __eq__(self, other) -> bool:
|
||||
return (self.start == other.start and
|
||||
self.end == other.end and
|
||||
self.points == other.points and
|
||||
self.segments == other.segments and
|
||||
self.properties == other.properties)
|
||||
return (
|
||||
self.start == other.start
|
||||
and self.end == other.end
|
||||
and self.points == other.points
|
||||
and self.segments == other.segments
|
||||
and self.properties == other.properties
|
||||
)
|
||||
|
||||
def copy(self) -> 'Sweep':
|
||||
return Sweep(self.start, self.end, self.points, self.segments,
|
||||
self.properties)
|
||||
def copy(self) -> "Sweep":
|
||||
return Sweep(
|
||||
self.start, self.end, self.points, self.segments, self.properties
|
||||
)
|
||||
|
||||
@property
|
||||
def span(self) -> int:
|
||||
|
@ -86,11 +99,11 @@ class Sweep:
|
|||
|
||||
def check(self):
|
||||
if (
|
||||
self.segments <= 0
|
||||
or self.points <= 0
|
||||
or self.start <= 0
|
||||
or self.end <= 0
|
||||
or self.stepsize < 1
|
||||
self.segments <= 0
|
||||
or self.points <= 0
|
||||
or self.start <= 0
|
||||
or self.end <= 0
|
||||
or self.stepsize < 1
|
||||
):
|
||||
raise ValueError(f"Illegal sweep settings: {self}")
|
||||
|
||||
|
|
|
@ -42,9 +42,8 @@ def truncate(values: List[List[Tuple]], count: int) -> List[List[Tuple]]:
|
|||
for valueset in np.swapaxes(values, 0, 1).tolist():
|
||||
avg = complex(*np.average(valueset, 0))
|
||||
truncated.append(
|
||||
sorted(valueset,
|
||||
key=lambda v, a=avg:
|
||||
abs(a - complex(*v)))[:keep])
|
||||
sorted(valueset, key=lambda v, a=avg: abs(a - complex(*v)))[:keep]
|
||||
)
|
||||
return np.swapaxes(truncated, 0, 1).tolist()
|
||||
|
||||
|
||||
|
@ -87,7 +86,8 @@ class SweepWorker(QtCore.QRunnable):
|
|||
logger.info("Initializing SweepWorker")
|
||||
if not self.app.vna.connected():
|
||||
logger.debug(
|
||||
"Attempted to run without being connected to the NanoVNA")
|
||||
"Attempted to run without being connected to the NanoVNA"
|
||||
)
|
||||
self.running = False
|
||||
return
|
||||
|
||||
|
@ -106,8 +106,9 @@ class SweepWorker(QtCore.QRunnable):
|
|||
if sweep.segments > 1:
|
||||
start = sweep.start
|
||||
end = sweep.end
|
||||
logger.debug("Resetting NanoVNA sweep to full range: %d to %d",
|
||||
start, end)
|
||||
logger.debug(
|
||||
"Resetting NanoVNA sweep to full range: %d to %d", start, end
|
||||
)
|
||||
self.app.vna.resetSweep(start, end)
|
||||
|
||||
self.percentage = 100
|
||||
|
@ -117,9 +118,11 @@ class SweepWorker(QtCore.QRunnable):
|
|||
|
||||
def _run_loop(self) -> None:
|
||||
sweep = self.sweep
|
||||
averages = (sweep.properties.averages[0]
|
||||
if sweep.properties.mode == SweepMode.AVERAGE
|
||||
else 1)
|
||||
averages = (
|
||||
sweep.properties.averages[0]
|
||||
if sweep.properties.mode == SweepMode.AVERAGE
|
||||
else 1
|
||||
)
|
||||
logger.info("%d averages", averages)
|
||||
|
||||
while True:
|
||||
|
@ -131,7 +134,8 @@ class SweepWorker(QtCore.QRunnable):
|
|||
start, stop = sweep.get_index_range(i)
|
||||
|
||||
freq, values11, values21 = self.readAveragedSegment(
|
||||
start, stop, averages)
|
||||
start, stop, averages
|
||||
)
|
||||
self.percentage = (i + 1) * 100 / sweep.segments
|
||||
self.updateData(freq, values11, values21, i)
|
||||
if sweep.properties.mode != SweepMode.CONTINOUS or self.stopped:
|
||||
|
@ -152,14 +156,18 @@ class SweepWorker(QtCore.QRunnable):
|
|||
def updateData(self, frequencies, values11, values21, index):
|
||||
# Update the data from (i*101) to (i+1)*101
|
||||
logger.debug(
|
||||
"Calculating data and inserting in existing data at index %d",
|
||||
index)
|
||||
"Calculating data and inserting in existing data at index %d", index
|
||||
)
|
||||
offset = self.sweep.points * index
|
||||
|
||||
raw_data11 = [Datapoint(freq, values11[i][0], values11[i][1])
|
||||
for i, freq in enumerate(frequencies)]
|
||||
raw_data21 = [Datapoint(freq, values21[i][0], values21[i][1])
|
||||
for i, freq in enumerate(frequencies)]
|
||||
raw_data11 = [
|
||||
Datapoint(freq, values11[i][0], values11[i][1])
|
||||
for i, freq in enumerate(frequencies)
|
||||
]
|
||||
raw_data21 = [
|
||||
Datapoint(freq, values21[i][0], values21[i][1])
|
||||
for i, freq in enumerate(frequencies)
|
||||
]
|
||||
|
||||
data11, data21 = self.applyCalibration(raw_data11, raw_data21)
|
||||
logger.debug("update Freqs: %s, Offset: %s", len(frequencies), offset)
|
||||
|
@ -169,16 +177,18 @@ class SweepWorker(QtCore.QRunnable):
|
|||
self.rawData11[offset + i] = raw_data11[i]
|
||||
self.rawData21[offset + i] = raw_data21[i]
|
||||
|
||||
logger.debug("Saving data to application (%d and %d points)",
|
||||
len(self.data11), len(self.data21))
|
||||
logger.debug(
|
||||
"Saving data to application (%d and %d points)",
|
||||
len(self.data11),
|
||||
len(self.data21),
|
||||
)
|
||||
self.app.saveData(self.data11, self.data21)
|
||||
logger.debug('Sending "updated" signal')
|
||||
self.signals.updated.emit()
|
||||
|
||||
def applyCalibration(self,
|
||||
raw_data11: List[Datapoint],
|
||||
raw_data21: List[Datapoint]
|
||||
) -> Tuple[List[Datapoint], List[Datapoint]]:
|
||||
def applyCalibration(
|
||||
self, raw_data11: List[Datapoint], raw_data21: List[Datapoint]
|
||||
) -> Tuple[List[Datapoint], List[Datapoint]]:
|
||||
data11: List[Datapoint] = []
|
||||
data21: List[Datapoint] = []
|
||||
|
||||
|
@ -186,8 +196,9 @@ class SweepWorker(QtCore.QRunnable):
|
|||
data11 = raw_data11.copy()
|
||||
data21 = raw_data21.copy()
|
||||
elif self.app.calibration.isValid1Port():
|
||||
data11.extend(self.app.calibration.correct11(dp)
|
||||
for dp in raw_data11)
|
||||
data11.extend(
|
||||
self.app.calibration.correct11(dp) for dp in raw_data11
|
||||
)
|
||||
else:
|
||||
data11 = raw_data11.copy()
|
||||
|
||||
|
@ -199,8 +210,10 @@ class SweepWorker(QtCore.QRunnable):
|
|||
data21 = raw_data21
|
||||
|
||||
if self.offsetDelay != 0:
|
||||
data11 = [correct_delay(dp, self.offsetDelay, reflect=True)
|
||||
for dp in data11]
|
||||
data11 = [
|
||||
correct_delay(dp, self.offsetDelay, reflect=True)
|
||||
for dp in data11
|
||||
]
|
||||
data21 = [correct_delay(dp, self.offsetDelay) for dp in data21]
|
||||
|
||||
return data11, data21
|
||||
|
@ -209,8 +222,9 @@ class SweepWorker(QtCore.QRunnable):
|
|||
values11 = []
|
||||
values21 = []
|
||||
freq = []
|
||||
logger.info("Reading from %d to %d. Averaging %d values",
|
||||
start, stop, averages)
|
||||
logger.info(
|
||||
"Reading from %d to %d. Averaging %d values", start, stop, averages
|
||||
)
|
||||
for i in range(averages):
|
||||
if self.stopped:
|
||||
logger.debug("Stopping averaging as signalled.")
|
||||
|
@ -227,8 +241,9 @@ class SweepWorker(QtCore.QRunnable):
|
|||
retry += 1
|
||||
freq, tmp11, tmp21 = self.readSegment(start, stop)
|
||||
if retry > 1:
|
||||
logger.error("retry %s readSegment(%s,%s)",
|
||||
retry, start, stop)
|
||||
logger.error(
|
||||
"retry %s readSegment(%s,%s)", retry, start, stop
|
||||
)
|
||||
sleep(0.5)
|
||||
values11.append(tmp11)
|
||||
values21.append(tmp21)
|
||||
|
@ -240,8 +255,7 @@ class SweepWorker(QtCore.QRunnable):
|
|||
|
||||
truncates = self.sweep.properties.averages[1]
|
||||
if truncates > 0 and averages > 1:
|
||||
logger.debug("Truncating %d values by %d",
|
||||
len(values11), truncates)
|
||||
logger.debug("Truncating %d values by %d", len(values11), truncates)
|
||||
values11 = truncate(values11, truncates)
|
||||
values21 = truncate(values21, truncates)
|
||||
|
||||
|
@ -278,36 +292,42 @@ class SweepWorker(QtCore.QRunnable):
|
|||
a, b = d.split(" ")
|
||||
try:
|
||||
if self.app.vna.validateInput and (
|
||||
abs(float(a)) > 9.5 or
|
||||
abs(float(b)) > 9.5):
|
||||
abs(float(a)) > 9.5 or abs(float(b)) > 9.5
|
||||
):
|
||||
logger.warning(
|
||||
"Got a non plausible data value: (%s)", d)
|
||||
"Got a non plausible data value: (%s)", d
|
||||
)
|
||||
done = False
|
||||
break
|
||||
returndata.append((float(a), float(b)))
|
||||
except ValueError as exc:
|
||||
logger.exception("An exception occurred reading %s: %s",
|
||||
data, exc)
|
||||
logger.exception(
|
||||
"An exception occurred reading %s: %s", data, exc
|
||||
)
|
||||
done = False
|
||||
if not done:
|
||||
logger.debug("Re-reading %s", data)
|
||||
sleep(0.2)
|
||||
count += 1
|
||||
if count == 5:
|
||||
logger.error("Tried and failed to read %s %d times.",
|
||||
data, count)
|
||||
logger.error(
|
||||
"Tried and failed to read %s %d times.", data, count
|
||||
)
|
||||
logger.debug("trying to reconnect")
|
||||
self.app.vna.reconnect()
|
||||
if count >= 10:
|
||||
logger.critical(
|
||||
"Tried and failed to read %s %d times. Giving up.",
|
||||
data, count)
|
||||
data,
|
||||
count,
|
||||
)
|
||||
raise IOError(
|
||||
f"Failed reading {data} {count} times.\n"
|
||||
f"Data outside expected valid ranges,"
|
||||
f" or in an unexpected format.\n\n"
|
||||
f"You can disable data validation on the"
|
||||
f"device settings screen.")
|
||||
f"device settings screen."
|
||||
)
|
||||
return returndata
|
||||
|
||||
def gui_error(self, message: str):
|
||||
|
|
|
@ -35,20 +35,22 @@ class Options:
|
|||
# Fun fact: In Touchstone 1.1 spec all params are optional unordered.
|
||||
# Just the line has to start with "#"
|
||||
UNIT_TO_FACTOR = {
|
||||
"ghz": 10 ** 9,
|
||||
"mhz": 10 ** 6,
|
||||
"khz": 10 ** 3,
|
||||
"hz": 10 ** 0,
|
||||
"ghz": 10**9,
|
||||
"mhz": 10**6,
|
||||
"khz": 10**3,
|
||||
"hz": 10**0,
|
||||
}
|
||||
VALID_UNITS = UNIT_TO_FACTOR.keys()
|
||||
VALID_PARAMETERS = "syzgh"
|
||||
VALID_FORMATS = ("ma", "db", "ri")
|
||||
|
||||
def __init__(self,
|
||||
unit: str = "GHZ",
|
||||
parameter: str = "S",
|
||||
t_format: str = "ma",
|
||||
resistance: int = 50):
|
||||
def __init__(
|
||||
self,
|
||||
unit: str = "GHZ",
|
||||
parameter: str = "S",
|
||||
t_format: str = "ma",
|
||||
resistance: int = 50,
|
||||
):
|
||||
# set defaults
|
||||
assert unit.lower() in Options.VALID_UNITS
|
||||
assert parameter.lower() in Options.VALID_PARAMETERS
|
||||
|
@ -145,9 +147,11 @@ class Touchstone:
|
|||
return self.sdata[Touchstone.FIELD_ORDER.index(name)]
|
||||
|
||||
def s_freq(self, name: str, freq: int) -> Datapoint:
|
||||
return Datapoint(freq,
|
||||
float(self._interp[name]["real"](freq)),
|
||||
float(self._interp[name]["imag"](freq)))
|
||||
return Datapoint(
|
||||
freq,
|
||||
float(self._interp[name]["real"](freq)),
|
||||
float(self._interp[name]["imag"](freq)),
|
||||
)
|
||||
|
||||
def swap(self):
|
||||
self.sdata = [self.s22, self.s12, self.s21, self.s11]
|
||||
|
@ -170,12 +174,20 @@ class Touchstone:
|
|||
imag.append(dp.im)
|
||||
|
||||
self._interp[i] = {
|
||||
"real": interp1d(freq, real,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(real[0], real[-1])),
|
||||
"imag": interp1d(freq, imag,
|
||||
kind="slinear", bounds_error=False,
|
||||
fill_value=(imag[0], imag[-1])),
|
||||
"real": interp1d(
|
||||
freq,
|
||||
real,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(real[0], real[-1]),
|
||||
),
|
||||
"imag": interp1d(
|
||||
freq,
|
||||
imag,
|
||||
kind="slinear",
|
||||
bounds_error=False,
|
||||
fill_value=(imag[0], imag[-1]),
|
||||
),
|
||||
}
|
||||
|
||||
def _parse_comments(self, fp) -> str:
|
||||
|
@ -192,27 +204,29 @@ class Touchstone:
|
|||
vals = iter(data)
|
||||
for v in vals:
|
||||
if self.opts.format == "ri":
|
||||
next(data_list).append(Datapoint(freq, float(v),
|
||||
float(next(vals))))
|
||||
next(data_list).append(
|
||||
Datapoint(freq, float(v), float(next(vals)))
|
||||
)
|
||||
if self.opts.format == "ma":
|
||||
z = cmath.rect(float(v), math.radians(float(next(vals))))
|
||||
next(data_list).append(Datapoint(freq, z.real, z.imag))
|
||||
if self.opts.format == "db":
|
||||
z = cmath.rect(10 ** (float(v) / 20),
|
||||
math.radians(float(next(vals))))
|
||||
z = cmath.rect(
|
||||
10 ** (float(v) / 20), math.radians(float(next(vals)))
|
||||
)
|
||||
next(data_list).append(Datapoint(freq, z.real, z.imag))
|
||||
|
||||
def load(self):
|
||||
logger.info("Attempting to open file %s", self.filename)
|
||||
try:
|
||||
with open(self.filename, encoding='utf-8') as infile:
|
||||
with open(self.filename, encoding="utf-8") as infile:
|
||||
self.loads(infile.read())
|
||||
except IOError as e:
|
||||
logger.exception("Failed to open %s: %s", self.filename, e)
|
||||
|
||||
def loads(self, s: str):
|
||||
"""Parse touchstone 1.1 string input
|
||||
appends to existing sdata if Touchstone object exists
|
||||
appends to existing sdata if Touchstone object exists
|
||||
"""
|
||||
try:
|
||||
self._loads(s)
|
||||
|
@ -239,7 +253,7 @@ class Touchstone:
|
|||
continue
|
||||
|
||||
# ignore comments at data end
|
||||
data = line.split('!')[0]
|
||||
data = line.split("!")[0]
|
||||
data = data.split()
|
||||
freq, data = round(float(data[0]) * self.opts.factor), data[1:]
|
||||
data_len = len(data)
|
||||
|
@ -270,8 +284,7 @@ class Touchstone:
|
|||
nr_params: Number of s-parameters. 2 for s1p, 4 for s2p
|
||||
"""
|
||||
|
||||
logger.info("Attempting to open file %s for writing",
|
||||
self.filename)
|
||||
logger.info("Attempting to open file %s for writing", self.filename)
|
||||
with open(self.filename, "w", encoding="utf-8") as outfile:
|
||||
outfile.write(self.saves(nr_params))
|
||||
|
||||
|
|
|
@ -22,13 +22,16 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class Version:
|
||||
RXP = re.compile(r"""^
|
||||
RXP = re.compile(
|
||||
r"""^
|
||||
\D*
|
||||
(?P<major>\d+)\.
|
||||
(?P<minor>\d+)\.?
|
||||
(?P<revision>\d+)?
|
||||
(?P<note>.*)
|
||||
$""", re.VERBOSE)
|
||||
$""",
|
||||
re.VERBOSE,
|
||||
)
|
||||
|
||||
def __init__(self, vstring: str = "0.0.0"):
|
||||
self.data = {
|
||||
|
@ -68,8 +71,10 @@ class Version:
|
|||
return self.data == other.data
|
||||
|
||||
def __str__(self) -> str:
|
||||
return (f'{self.data["major"]}.{self.data["minor"]}'
|
||||
f'.{self.data["revision"]}{self.data["note"]}')
|
||||
return (
|
||||
f'{self.data["major"]}.{self.data["minor"]}'
|
||||
f'.{self.data["revision"]}{self.data["note"]}'
|
||||
)
|
||||
|
||||
@property
|
||||
def major(self) -> int:
|
||||
|
|
|
@ -53,28 +53,36 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
layout = QtWidgets.QVBoxLayout()
|
||||
top_layout.addLayout(layout)
|
||||
|
||||
layout.addWidget(QtWidgets.QLabel(
|
||||
f"NanoVNASaver version {self.app.version}"))
|
||||
layout.addWidget(
|
||||
QtWidgets.QLabel(f"NanoVNASaver version {self.app.version}")
|
||||
)
|
||||
layout.addWidget(QtWidgets.QLabel(""))
|
||||
layout.addWidget(QtWidgets.QLabel(
|
||||
"\N{COPYRIGHT SIGN} Copyright 2019, 2020 Rune B. Broberg\n"
|
||||
"\N{COPYRIGHT SIGN} Copyright 2020ff NanoVNA-Saver Authors"
|
||||
))
|
||||
layout.addWidget(QtWidgets.QLabel(
|
||||
"This program comes with ABSOLUTELY NO WARRANTY"))
|
||||
layout.addWidget(QtWidgets.QLabel(
|
||||
"This program is licensed under the"
|
||||
" GNU General Public License version 3"))
|
||||
layout.addWidget(
|
||||
QtWidgets.QLabel(
|
||||
"\N{COPYRIGHT SIGN} Copyright 2019, 2020 Rune B. Broberg\n"
|
||||
"\N{COPYRIGHT SIGN} Copyright 2020ff NanoVNA-Saver Authors"
|
||||
)
|
||||
)
|
||||
layout.addWidget(
|
||||
QtWidgets.QLabel("This program comes with ABSOLUTELY NO WARRANTY")
|
||||
)
|
||||
layout.addWidget(
|
||||
QtWidgets.QLabel(
|
||||
"This program is licensed under the"
|
||||
" GNU General Public License version 3"
|
||||
)
|
||||
)
|
||||
layout.addWidget(QtWidgets.QLabel(""))
|
||||
link_label = QtWidgets.QLabel(
|
||||
f'For further details, see: <a href="{INFO_URL}">'
|
||||
f"{INFO_URL}")
|
||||
f'For further details, see: <a href="{INFO_URL}">' f"{INFO_URL}"
|
||||
)
|
||||
link_label.setOpenExternalLinks(True)
|
||||
layout.addWidget(link_label)
|
||||
layout.addWidget(QtWidgets.QLabel(""))
|
||||
|
||||
self.versionLabel = QtWidgets.QLabel(
|
||||
"NanoVNA Firmware Version: Not connected.")
|
||||
"NanoVNA Firmware Version: Not connected."
|
||||
)
|
||||
layout.addWidget(self.versionLabel)
|
||||
|
||||
layout.addStretch()
|
||||
|
@ -106,14 +114,15 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
with contextlib.suppress(IOError, AttributeError):
|
||||
self.versionLabel.setText(
|
||||
f"NanoVNA Firmware Version: {self.app.vna.name} "
|
||||
f"v{self.app.vna.version}")
|
||||
f"v{self.app.vna.version}"
|
||||
)
|
||||
|
||||
def findUpdates(self, automatic=False):
|
||||
latest_version = Version()
|
||||
latest_url = ""
|
||||
try:
|
||||
req = request.Request(VERSION_URL)
|
||||
req.add_header('User-Agent', f'NanoVNA-Saver/{self.app.version}')
|
||||
req.add_header("User-Agent", f"NanoVNA-Saver/{self.app.version}")
|
||||
for line in request.urlopen(req, timeout=3):
|
||||
line = line.decode("utf-8")
|
||||
if line.startswith("VERSION ="):
|
||||
|
@ -122,17 +131,20 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
latest_url = line[13:].strip(" \"'")
|
||||
except error.HTTPError as e:
|
||||
logger.exception(
|
||||
"Checking for updates produced an HTTP exception: %s", e)
|
||||
"Checking for updates produced an HTTP exception: %s", e
|
||||
)
|
||||
self.updateLabel.setText("Connection error.")
|
||||
return
|
||||
except TypeError as e:
|
||||
logger.exception(
|
||||
"Checking for updates provided an unparseable file: %s", e)
|
||||
"Checking for updates provided an unparseable file: %s", e
|
||||
)
|
||||
self.updateLabel.setText("Data error reading versions.")
|
||||
return
|
||||
except error.URLError as e:
|
||||
logger.exception(
|
||||
"Checking for updates produced a URL exception: %s", e)
|
||||
"Checking for updates produced a URL exception: %s", e
|
||||
)
|
||||
self.updateLabel.setText("Connection error.")
|
||||
return
|
||||
|
||||
|
@ -147,13 +159,17 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
"Updates available",
|
||||
f"There is a new update for NanoVNA-Saver available!\n"
|
||||
f"Version {latest_version}\n\n"
|
||||
f'Press "About" to find the update.')
|
||||
f'Press "About" to find the update.',
|
||||
)
|
||||
else:
|
||||
QtWidgets.QMessageBox.information(
|
||||
self, "Updates available",
|
||||
"There is a new update for NanoVNA-Saver available!")
|
||||
self,
|
||||
"Updates available",
|
||||
"There is a new update for NanoVNA-Saver available!",
|
||||
)
|
||||
self.updateLabel.setText(
|
||||
f'<a href="{latest_url}">New version available</a>.')
|
||||
f'<a href="{latest_url}">New version available</a>.'
|
||||
)
|
||||
self.updateLabel.setOpenExternalLinks(True)
|
||||
else:
|
||||
# Probably don't show a message box, just update the screen?
|
||||
|
@ -161,5 +177,6 @@ class AboutWindow(QtWidgets.QWidget):
|
|||
#
|
||||
self.updateLabel.setText(
|
||||
f"Last checked: "
|
||||
f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}")
|
||||
f"{strftime('%Y-%m-%d %H:%M:%S', localtime())}"
|
||||
)
|
||||
return
|
||||
|
|
|
@ -29,7 +29,9 @@ from NanoVNASaver.Analysis.HighPassAnalysis import HighPassAnalysis
|
|||
from NanoVNASaver.Analysis.LowPassAnalysis import LowPassAnalysis
|
||||
from NanoVNASaver.Analysis.PeakSearchAnalysis import PeakSearchAnalysis
|
||||
from NanoVNASaver.Analysis.ResonanceAnalysis import ResonanceAnalysis
|
||||
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import SimplePeakSearchAnalysis
|
||||
from NanoVNASaver.Analysis.SimplePeakSearchAnalysis import (
|
||||
SimplePeakSearchAnalysis,
|
||||
)
|
||||
from NanoVNASaver.Analysis.VSWRAnalysis import VSWRAnalysis
|
||||
from NanoVNASaver.Windows.Defaults import make_scrollable
|
||||
|
||||
|
@ -55,25 +57,28 @@ class AnalysisWindow(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(
|
||||
"Low-pass filter", LowPassAnalysis(self.app))
|
||||
"Band-pass filter", BandPassAnalysis(self.app)
|
||||
)
|
||||
self.analysis_list.addItem(
|
||||
"Band-pass filter", BandPassAnalysis(self.app))
|
||||
"High-pass filter", HighPassAnalysis(self.app)
|
||||
)
|
||||
self.analysis_list.addItem(
|
||||
"High-pass filter", HighPassAnalysis(self.app))
|
||||
"Band-stop filter", BandStopAnalysis(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))
|
||||
"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(
|
||||
"Resonance analysis", ResonanceAnalysis(self.app))
|
||||
"Resonance analysis", ResonanceAnalysis(self.app)
|
||||
)
|
||||
self.analysis_list.addItem("HWEF analysis", EFHWAnalysis(self.app))
|
||||
self.analysis_list.addItem(
|
||||
"HWEF analysis", EFHWAnalysis(self.app))
|
||||
self.analysis_list.addItem(
|
||||
"MagLoop analysis", MagLoopAnalysis(self.app))
|
||||
"MagLoop analysis", MagLoopAnalysis(self.app)
|
||||
)
|
||||
select_analysis_layout.addRow("Analysis type", self.analysis_list)
|
||||
self.analysis_list.currentIndexChanged.connect(self.updateSelection)
|
||||
|
||||
|
@ -82,15 +87,18 @@ class AnalysisWindow(QtWidgets.QWidget):
|
|||
select_analysis_layout.addRow(btn_run_analysis)
|
||||
|
||||
self.checkbox_run_automatically = QtWidgets.QCheckBox(
|
||||
"Run automatically")
|
||||
"Run automatically"
|
||||
)
|
||||
self.checkbox_run_automatically.stateChanged.connect(
|
||||
self.toggleAutomaticRun)
|
||||
self.toggleAutomaticRun
|
||||
)
|
||||
select_analysis_layout.addRow(self.checkbox_run_automatically)
|
||||
|
||||
analysis_box = QtWidgets.QGroupBox("Analysis")
|
||||
analysis_box.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
)
|
||||
|
||||
self.analysis_layout = QtWidgets.QVBoxLayout(analysis_box)
|
||||
self.analysis_layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
@ -110,7 +118,8 @@ class AnalysisWindow(QtWidgets.QWidget):
|
|||
if old_item is not None:
|
||||
old_widget = self.analysis_layout.itemAt(0).widget()
|
||||
self.analysis_layout.replaceWidget(
|
||||
old_widget, self.analysis.widget())
|
||||
old_widget, self.analysis.widget()
|
||||
)
|
||||
old_widget.hide()
|
||||
else:
|
||||
self.analysis_layout.addWidget(self.analysis.widget())
|
||||
|
|
|
@ -66,6 +66,7 @@ class BandsWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QMessageBox.Warning,
|
||||
"Confirm reset",
|
||||
"Are you sure you want to reset the bands to default?",
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel).exec()
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel,
|
||||
).exec()
|
||||
if confirm == QtWidgets.QMessageBox.Yes:
|
||||
self.app.bands.resetBands()
|
||||
|
|
|
@ -50,8 +50,10 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.setMinimumWidth(450)
|
||||
self.setWindowTitle("Calibration")
|
||||
self.setWindowIcon(self.app.icon)
|
||||
self.setSizePolicy(QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding)
|
||||
self.setSizePolicy(
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
QtWidgets.QSizePolicy.MinimumExpanding,
|
||||
)
|
||||
|
||||
QtWidgets.QShortcut(QtCore.Qt.Key_Escape, self, self.hide)
|
||||
|
||||
|
@ -67,28 +69,38 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
calibration_status_layout = QtWidgets.QFormLayout()
|
||||
self.calibration_status_label = QtWidgets.QLabel("Device calibration")
|
||||
self.calibration_source_label = QtWidgets.QLabel("NanoVNA")
|
||||
calibration_status_layout.addRow("Calibration:",
|
||||
self.calibration_status_label)
|
||||
calibration_status_layout.addRow("Source:",
|
||||
self.calibration_source_label)
|
||||
calibration_status_layout.addRow(
|
||||
"Calibration:", self.calibration_status_label
|
||||
)
|
||||
calibration_status_layout.addRow(
|
||||
"Source:", self.calibration_source_label
|
||||
)
|
||||
calibration_status_group.setLayout(calibration_status_layout)
|
||||
left_layout.addWidget(calibration_status_group)
|
||||
|
||||
calibration_control_group = QtWidgets.QGroupBox("Calibrate")
|
||||
calibration_control_layout = QtWidgets.QFormLayout(
|
||||
calibration_control_group)
|
||||
calibration_control_group
|
||||
)
|
||||
cal_btn = {}
|
||||
self.cal_label = {}
|
||||
for label_name in ("short", "open", "load",
|
||||
"through", "thrurefl", "isolation"):
|
||||
for label_name in (
|
||||
"short",
|
||||
"open",
|
||||
"load",
|
||||
"through",
|
||||
"thrurefl",
|
||||
"isolation",
|
||||
):
|
||||
self.cal_label[label_name] = QtWidgets.QLabel("Uncalibrated")
|
||||
cal_btn[label_name] = QtWidgets.QPushButton(
|
||||
label_name.capitalize())
|
||||
cal_btn[label_name] = QtWidgets.QPushButton(label_name.capitalize())
|
||||
cal_btn[label_name].setMinimumHeight(20)
|
||||
cal_btn[label_name].clicked.connect(
|
||||
partial(self.manual_save, label_name))
|
||||
partial(self.manual_save, label_name)
|
||||
)
|
||||
calibration_control_layout.addRow(
|
||||
cal_btn[label_name], self.cal_label[label_name])
|
||||
cal_btn[label_name], self.cal_label[label_name]
|
||||
)
|
||||
|
||||
self.input_offset_delay = QtWidgets.QDoubleSpinBox()
|
||||
self.input_offset_delay.setMinimumHeight(20)
|
||||
|
@ -100,7 +112,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
|
||||
calibration_control_layout.addRow(QtWidgets.QLabel(""))
|
||||
calibration_control_layout.addRow(
|
||||
"Offset delay", self.input_offset_delay)
|
||||
"Offset delay", self.input_offset_delay
|
||||
)
|
||||
|
||||
self.btn_automatic = QtWidgets.QPushButton("Calibration assistant")
|
||||
self.btn_automatic.setMinimumHeight(20)
|
||||
|
@ -126,7 +139,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
|
||||
calibration_notes_group = QtWidgets.QGroupBox("Notes")
|
||||
calibration_notes_layout = QtWidgets.QVBoxLayout(
|
||||
calibration_notes_group)
|
||||
calibration_notes_group
|
||||
)
|
||||
self.notes_textedit = QtWidgets.QPlainTextEdit()
|
||||
calibration_notes_layout.addWidget(self.notes_textedit)
|
||||
|
||||
|
@ -225,7 +239,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
|
||||
self.cal_standard_save_box = QtWidgets.QGroupBox("Saved settings")
|
||||
cal_standard_save_layout = QtWidgets.QVBoxLayout(
|
||||
self.cal_standard_save_box)
|
||||
self.cal_standard_save_box
|
||||
)
|
||||
self.cal_standard_save_box.setDisabled(True)
|
||||
|
||||
self.cal_standard_save_selector = QtWidgets.QComboBox()
|
||||
|
@ -253,7 +268,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
def checkExpertUser(self):
|
||||
if not self.app.settings.value("ExpertCalibrationUser", False, bool):
|
||||
response = QtWidgets.QMessageBox.question(
|
||||
self, "Are you sure?",
|
||||
self,
|
||||
"Are you sure?",
|
||||
(
|
||||
"Use of the manual calibration buttons is non-intuitive,"
|
||||
" and primarily suited for users with very specialized"
|
||||
|
@ -267,7 +283,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
" Yes."
|
||||
),
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Cancel,
|
||||
QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
if response == QtWidgets.QMessageBox.Yes:
|
||||
self.app.settings.setValue("ExpertCalibrationUser", True)
|
||||
|
@ -280,8 +297,7 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.app.calibration.insert(name, self.app.data.s21)
|
||||
else:
|
||||
self.app.calibration.insert(name, self.app.data.s11)
|
||||
self.cal_label[name].setText(
|
||||
_format_cal_label(len(self.app.data.s11)))
|
||||
self.cal_label[name].setText(_format_cal_label(len(self.app.data.s11)))
|
||||
|
||||
def manual_save(self, name: str):
|
||||
if self.checkExpertUser():
|
||||
|
@ -289,8 +305,7 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
|
||||
def listCalibrationStandards(self):
|
||||
self.cal_standard_save_selector.clear()
|
||||
num_standards = self.app.settings.beginReadArray(
|
||||
"CalibrationStandards")
|
||||
num_standards = self.app.settings.beginReadArray("CalibrationStandards")
|
||||
for i in range(num_standards):
|
||||
self.app.settings.setArrayIndex(i)
|
||||
name = self.app.settings.value("Name", defaultValue="INVALID NAME")
|
||||
|
@ -300,15 +315,15 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.cal_standard_save_selector.setCurrentText("New")
|
||||
|
||||
def saveCalibrationStandard(self):
|
||||
num_standards = self.app.settings.beginReadArray(
|
||||
"CalibrationStandards")
|
||||
num_standards = self.app.settings.beginReadArray("CalibrationStandards")
|
||||
self.app.settings.endArray()
|
||||
|
||||
if self.cal_standard_save_selector.currentData() == -1:
|
||||
# New cal standard
|
||||
# Get a name
|
||||
name, selected = QtWidgets.QInputDialog.getText(
|
||||
self, "Calibration standard name", "Enter name to save as")
|
||||
self, "Calibration standard name", "Enter name to save as"
|
||||
)
|
||||
if not selected or not name:
|
||||
return
|
||||
write_num = num_standards
|
||||
|
@ -317,8 +332,7 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
write_num = self.cal_standard_save_selector.currentData()
|
||||
name = self.cal_standard_save_selector.currentText()
|
||||
|
||||
self.app.settings.beginWriteArray(
|
||||
"CalibrationStandards", num_standards)
|
||||
self.app.settings.beginWriteArray("CalibrationStandards", num_standards)
|
||||
self.app.settings.setArrayIndex(write_num)
|
||||
self.app.settings.setValue("Name", name)
|
||||
|
||||
|
@ -361,8 +375,7 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.short_l1_input.setText(str(self.app.settings.value("ShortL1", 0)))
|
||||
self.short_l2_input.setText(str(self.app.settings.value("ShortL2", 0)))
|
||||
self.short_l3_input.setText(str(self.app.settings.value("ShortL3", 0)))
|
||||
self.short_length.setText(
|
||||
str(self.app.settings.value("ShortDelay", 0)))
|
||||
self.short_length.setText(str(self.app.settings.value("ShortDelay", 0)))
|
||||
|
||||
self.open_c0_input.setText(str(self.app.settings.value("OpenC0", 50)))
|
||||
self.open_c1_input.setText(str(self.app.settings.value("OpenC1", 0)))
|
||||
|
@ -376,7 +389,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.load_length.setText(str(self.app.settings.value("LoadDelay", 0)))
|
||||
|
||||
self.through_length.setText(
|
||||
str(self.app.settings.value("ThroughDelay", 0)))
|
||||
str(self.app.settings.value("ThroughDelay", 0))
|
||||
)
|
||||
|
||||
self.app.settings.endArray()
|
||||
|
||||
|
@ -385,8 +399,7 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
return
|
||||
delete_num = self.cal_standard_save_selector.currentData()
|
||||
logger.debug("Deleting calibration no %d", delete_num)
|
||||
num_standards = self.app.settings.beginReadArray(
|
||||
"CalibrationStandards")
|
||||
num_standards = self.app.settings.beginReadArray("CalibrationStandards")
|
||||
self.app.settings.endArray()
|
||||
|
||||
logger.debug("Number of standards known: %d", num_standards)
|
||||
|
@ -449,7 +462,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.app.settings.endArray()
|
||||
|
||||
self.app.settings.beginWriteArray(
|
||||
"CalibrationStandards", len(names))
|
||||
"CalibrationStandards", len(names)
|
||||
)
|
||||
for i, name in enumerate(names):
|
||||
self.app.settings.setArrayIndex(i)
|
||||
self.app.settings.setValue("Name", name)
|
||||
|
@ -488,8 +502,11 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
if len(self.app.worker.rawData11) > 0:
|
||||
# There's raw data, so we can get corrected data
|
||||
logger.debug("Saving and displaying raw data.")
|
||||
self.app.saveData(self.app.worker.rawData11,
|
||||
self.app.worker.rawData21, self.app.sweepSource)
|
||||
self.app.saveData(
|
||||
self.app.worker.rawData11,
|
||||
self.app.worker.rawData21,
|
||||
self.app.sweepSource,
|
||||
)
|
||||
self.app.worker.signals.updated.emit()
|
||||
|
||||
def setOffsetDelay(self, value: float):
|
||||
|
@ -498,12 +515,18 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
if len(self.app.worker.rawData11) > 0:
|
||||
# There's raw data, so we can get corrected data
|
||||
logger.debug("Applying new offset to existing sweep data.")
|
||||
self.app.worker.data11, self.app.worker.data21 = \
|
||||
self.app.worker.applyCalibration(
|
||||
self.app.worker.rawData11, self.app.worker.rawData21)
|
||||
(
|
||||
self.app.worker.data11,
|
||||
self.app.worker.data21,
|
||||
) = self.app.worker.applyCalibration(
|
||||
self.app.worker.rawData11, self.app.worker.rawData21
|
||||
)
|
||||
logger.debug("Saving and displaying corrected data.")
|
||||
self.app.saveData(self.app.worker.data11,
|
||||
self.app.worker.data21, self.app.sweepSource)
|
||||
self.app.saveData(
|
||||
self.app.worker.data11,
|
||||
self.app.worker.data21,
|
||||
self.app.sweepSource,
|
||||
)
|
||||
self.app.worker.signals.updated.emit()
|
||||
|
||||
def calculate(self):
|
||||
|
@ -511,7 +534,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
if self.app.sweep_control.btn_stop.isEnabled():
|
||||
self.app.showError(
|
||||
"Unable to apply calibration while a sweep is running."
|
||||
" Please stop the sweep and try again.")
|
||||
" Please stop the sweep and try again."
|
||||
)
|
||||
return
|
||||
|
||||
cal_element.short_is_ideal = True
|
||||
|
@ -528,63 +552,85 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
|
||||
# We are using custom calibration standards
|
||||
|
||||
cal_element.short_l0 = getFloatValue(
|
||||
self.short_l0_input.text()) / 1.0e12
|
||||
cal_element.short_l1 = getFloatValue(
|
||||
self.short_l1_input.text()) / 1.0e24
|
||||
cal_element.short_l2 = getFloatValue(
|
||||
self.short_l2_input.text()) / 1.0e33
|
||||
cal_element.short_l3 = getFloatValue(
|
||||
self.short_l3_input.text()) / 1.0e42
|
||||
cal_element.short_length = getFloatValue(
|
||||
self.short_length.text()) / 1.0e12
|
||||
cal_element.short_l0 = (
|
||||
getFloatValue(self.short_l0_input.text()) / 1.0e12
|
||||
)
|
||||
cal_element.short_l1 = (
|
||||
getFloatValue(self.short_l1_input.text()) / 1.0e24
|
||||
)
|
||||
cal_element.short_l2 = (
|
||||
getFloatValue(self.short_l2_input.text()) / 1.0e33
|
||||
)
|
||||
cal_element.short_l3 = (
|
||||
getFloatValue(self.short_l3_input.text()) / 1.0e42
|
||||
)
|
||||
cal_element.short_length = (
|
||||
getFloatValue(self.short_length.text()) / 1.0e12
|
||||
)
|
||||
|
||||
cal_element.open_c0 = getFloatValue(
|
||||
self.open_c0_input.text()) / 1.e15
|
||||
cal_element.open_c1 = getFloatValue(
|
||||
self.open_c1_input.text()) / 1.e27
|
||||
cal_element.open_c2 = getFloatValue(
|
||||
self.open_c2_input.text()) / 1.0e36
|
||||
cal_element.open_c3 = getFloatValue(
|
||||
self.open_c3_input.text()) / 1.0e45
|
||||
cal_element.openLength = getFloatValue(
|
||||
self.open_length.text()) / 1.0e12
|
||||
cal_element.open_c0 = (
|
||||
getFloatValue(self.open_c0_input.text()) / 1.0e15
|
||||
)
|
||||
cal_element.open_c1 = (
|
||||
getFloatValue(self.open_c1_input.text()) / 1.0e27
|
||||
)
|
||||
cal_element.open_c2 = (
|
||||
getFloatValue(self.open_c2_input.text()) / 1.0e36
|
||||
)
|
||||
cal_element.open_c3 = (
|
||||
getFloatValue(self.open_c3_input.text()) / 1.0e45
|
||||
)
|
||||
cal_element.openLength = (
|
||||
getFloatValue(self.open_length.text()) / 1.0e12
|
||||
)
|
||||
|
||||
cal_element.load_r = getFloatValue(
|
||||
self.load_resistance.text())
|
||||
cal_element.load_l = getFloatValue(
|
||||
self.load_inductance.text()) / 1.0e12
|
||||
cal_element.load_c = getFloatValue(
|
||||
self.load_capacitance.text()) / 1.0e15
|
||||
cal_element.load_length = getFloatValue(
|
||||
self.load_length.text()) / 1.0e12
|
||||
cal_element.load_r = getFloatValue(self.load_resistance.text())
|
||||
cal_element.load_l = (
|
||||
getFloatValue(self.load_inductance.text()) / 1.0e12
|
||||
)
|
||||
cal_element.load_c = (
|
||||
getFloatValue(self.load_capacitance.text()) / 1.0e15
|
||||
)
|
||||
cal_element.load_length = (
|
||||
getFloatValue(self.load_length.text()) / 1.0e12
|
||||
)
|
||||
|
||||
cal_element.through_length = getFloatValue(
|
||||
self.through_length.text()) / 1.0e12
|
||||
cal_element.through_length = (
|
||||
getFloatValue(self.through_length.text()) / 1.0e12
|
||||
)
|
||||
|
||||
logger.debug("Attempting calibration calculation.")
|
||||
try:
|
||||
self.app.calibration.calc_corrections()
|
||||
self.calibration_status_label.setText(
|
||||
_format_cal_label(self.app.calibration.size(),
|
||||
"Application calibration"))
|
||||
_format_cal_label(
|
||||
self.app.calibration.size(), "Application calibration"
|
||||
)
|
||||
)
|
||||
if self.use_ideal_values.isChecked():
|
||||
self.calibration_source_label.setText(
|
||||
self.app.calibration.source)
|
||||
self.app.calibration.source
|
||||
)
|
||||
else:
|
||||
self.calibration_source_label.setText(
|
||||
f"{self.app.calibration.source} (Standards: Custom)")
|
||||
f"{self.app.calibration.source} (Standards: Custom)"
|
||||
)
|
||||
|
||||
if self.app.worker.rawData11:
|
||||
# There's raw data, so we can get corrected data
|
||||
logger.debug("Applying calibration to existing sweep data.")
|
||||
self.app.worker.data11, self.app.worker.data21 = (
|
||||
self.app.worker.applyCalibration(
|
||||
self.app.worker.rawData11,
|
||||
self.app.worker.rawData21))
|
||||
(
|
||||
self.app.worker.data11,
|
||||
self.app.worker.data21,
|
||||
) = self.app.worker.applyCalibration(
|
||||
self.app.worker.rawData11, self.app.worker.rawData21
|
||||
)
|
||||
logger.debug("Saving and displaying corrected data.")
|
||||
self.app.saveData(self.app.worker.data11,
|
||||
self.app.worker.data21, self.app.sweepSource)
|
||||
self.app.saveData(
|
||||
self.app.worker.data11,
|
||||
self.app.worker.data21,
|
||||
self.app.sweepSource,
|
||||
)
|
||||
self.app.worker.signals.updated.emit()
|
||||
except ValueError as e:
|
||||
if logger.isEnabledFor(logging.DEBUG):
|
||||
|
@ -592,23 +638,29 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
# showError here hides the calibration window,
|
||||
# so we need to pop up our own
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "Error applying calibration", str(e))
|
||||
self, "Error applying calibration", str(e)
|
||||
)
|
||||
self.calibration_status_label.setText(
|
||||
"Applying calibration failed.")
|
||||
"Applying calibration failed."
|
||||
)
|
||||
self.calibration_source_label.setText(self.app.calibration.source)
|
||||
|
||||
def loadCalibration(self):
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
filter="Calibration Files (*.cal);;All files (*.*)")
|
||||
filter="Calibration Files (*.cal);;All files (*.*)"
|
||||
)
|
||||
if filename:
|
||||
self.app.calibration.load(filename)
|
||||
if not self.app.calibration.isValid1Port():
|
||||
return
|
||||
for i, name in enumerate(
|
||||
("short", "open", "load", "through", "isolation", "thrurefl")):
|
||||
("short", "open", "load", "through", "isolation", "thrurefl")
|
||||
):
|
||||
self.cal_label[name].setText(
|
||||
_format_cal_label(self.app.calibration.data_size(name),
|
||||
"Loaded"))
|
||||
_format_cal_label(
|
||||
self.app.calibration.data_size(name), "Loaded"
|
||||
)
|
||||
)
|
||||
if i == 2 and not self.app.calibration.isValid2Port():
|
||||
break
|
||||
self.calculate()
|
||||
|
@ -633,8 +685,9 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
if not filename:
|
||||
logger.debug("No file name selected.")
|
||||
return
|
||||
self.app.calibration.notes = self.notes_textedit.toPlainText(
|
||||
).splitlines()
|
||||
self.app.calibration.notes = (
|
||||
self.notes_textedit.toPlainText().splitlines()
|
||||
)
|
||||
try:
|
||||
self.app.calibration.save(filename)
|
||||
self.app.settings.setValue("CalibrationFile", filename)
|
||||
|
@ -648,7 +701,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
self.cal_load_box.setDisabled(self.use_ideal_values.isChecked())
|
||||
self.cal_through_box.setDisabled(self.use_ideal_values.isChecked())
|
||||
self.cal_standard_save_box.setDisabled(
|
||||
self.use_ideal_values.isChecked())
|
||||
self.use_ideal_values.isChecked()
|
||||
)
|
||||
|
||||
def automaticCalibration(self):
|
||||
self.btn_automatic.setDisabled(True)
|
||||
|
@ -662,14 +716,15 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
"Before starting, ensure you have Open, Short and Load"
|
||||
" standards available, and the cables you wish to have"
|
||||
" calibrated with the device connected.<br><br>"
|
||||
"If you want a 2-port calibration, also have a \"through\""
|
||||
'If you want a 2-port calibration, also have a "through"'
|
||||
" connector to hand.<br><br>"
|
||||
"<b>The best results are achieved by having the NanoVNA"
|
||||
" calibrated on-device for the full span of interest and saved"
|
||||
" to save slot 0 before starting.</b><br><br>"
|
||||
"Once you are ready to proceed, press Ok."
|
||||
),
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
response = introduction.exec()
|
||||
if response != QtWidgets.QMessageBox.Ok:
|
||||
self.btn_automatic.setDisabled(False)
|
||||
|
@ -679,8 +734,10 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QMessageBox(
|
||||
QtWidgets.QMessageBox.Information,
|
||||
"NanoVNA not connected",
|
||||
("Please ensure the NanoVNA is connected before attempting"
|
||||
" calibration.")
|
||||
(
|
||||
"Please ensure the NanoVNA is connected before attempting"
|
||||
" calibration."
|
||||
),
|
||||
).exec()
|
||||
self.btn_automatic.setDisabled(False)
|
||||
return
|
||||
|
@ -689,8 +746,10 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QMessageBox(
|
||||
QtWidgets.QMessageBox.Information,
|
||||
"Continuous sweep enabled",
|
||||
("Please disable continuous sweeping before attempting"
|
||||
" calibration.")
|
||||
(
|
||||
"Please disable continuous sweeping before attempting"
|
||||
" calibration."
|
||||
),
|
||||
).exec()
|
||||
self.btn_automatic.setDisabled(False)
|
||||
return
|
||||
|
@ -699,11 +758,12 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QMessageBox.Information,
|
||||
"Calibrate short",
|
||||
(
|
||||
"Please connect the \"short\" standard to port 0 of the"
|
||||
'Please connect the "short" standard to port 0 of the'
|
||||
" NanoVNA.\n\n"
|
||||
"Press Ok when you are ready to continue."
|
||||
),
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
response = short_step.exec()
|
||||
if response != QtWidgets.QMessageBox.Ok:
|
||||
|
@ -719,7 +779,8 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
def automaticCalibrationStep(self):
|
||||
if self.nextStep == -1:
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
|
||||
if self.nextStep == 0:
|
||||
|
@ -731,20 +792,22 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QMessageBox.Information,
|
||||
"Calibrate open",
|
||||
(
|
||||
"Please connect the \"open\" standard to port 0 of the"
|
||||
'Please connect the "open" standard to port 0 of the'
|
||||
" NanoVNA.\n\n"
|
||||
"Either use a supplied open, or leave the end of the"
|
||||
" cable unconnected if desired.\n\n"
|
||||
"Press Ok when you are ready to continue."
|
||||
),
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
response = open_step.exec()
|
||||
if response != QtWidgets.QMessageBox.Ok:
|
||||
self.nextStep = -1
|
||||
self.btn_automatic.setDisabled(False)
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
self.app.sweep_start()
|
||||
return
|
||||
|
@ -757,18 +820,20 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QMessageBox.Information,
|
||||
"Calibrate load",
|
||||
(
|
||||
"Please connect the \"load\" standard to port 0 of the"
|
||||
'Please connect the "load" standard to port 0 of the'
|
||||
" NanoVNA.\n\n"
|
||||
"Press Ok when you are ready to continue."
|
||||
),
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
response = load_step.exec()
|
||||
if response != QtWidgets.QMessageBox.Ok:
|
||||
self.btn_automatic.setDisabled(False)
|
||||
self.nextStep = -1
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
self.app.sweep_start()
|
||||
return
|
||||
|
@ -784,45 +849,51 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
"The required steps for a 1-port calibration are now"
|
||||
" complete.\n\n"
|
||||
"If you wish to continue and perform a 2-port calibration,"
|
||||
" press \"Yes\". To apply the 1-port calibration and stop,"
|
||||
" press \"Apply\""
|
||||
' press "Yes". To apply the 1-port calibration and stop,'
|
||||
' press "Apply"'
|
||||
),
|
||||
QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.Apply |
|
||||
QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Yes
|
||||
| QtWidgets.QMessageBox.Apply
|
||||
| QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
response = continue_step.exec()
|
||||
if response == QtWidgets.QMessageBox.Apply:
|
||||
self.calculate()
|
||||
self.nextStep = -1
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
self.btn_automatic.setDisabled(False)
|
||||
return
|
||||
if response != QtWidgets.QMessageBox.Yes:
|
||||
self.btn_automatic.setDisabled(False)
|
||||
self.nextStep = -1
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
|
||||
isolation_step = QtWidgets.QMessageBox(
|
||||
QtWidgets.QMessageBox.Information,
|
||||
"Calibrate isolation",
|
||||
(
|
||||
"Please connect the \"load\" standard to port 1 of the"
|
||||
'Please connect the "load" standard to port 1 of the'
|
||||
" NanoVNA.\n\n"
|
||||
"If available, also connect a load standard to"
|
||||
" port 0.\n\n"
|
||||
"Press Ok when you are ready to continue."
|
||||
),
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
response = isolation_step.exec()
|
||||
if response != QtWidgets.QMessageBox.Ok:
|
||||
self.btn_automatic.setDisabled(False)
|
||||
self.nextStep = -1
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
self.app.sweep_start()
|
||||
return
|
||||
|
@ -835,18 +906,20 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
QtWidgets.QMessageBox.Information,
|
||||
"Calibrate through",
|
||||
(
|
||||
"Please connect the \"through\" standard between"
|
||||
'Please connect the "through" standard between'
|
||||
" port 0 and port 1 of the NanoVNA.\n\n"
|
||||
"Press Ok when you are ready to continue."
|
||||
),
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Ok | QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
response = through_step.exec()
|
||||
if response != QtWidgets.QMessageBox.Ok:
|
||||
self.btn_automatic.setDisabled(False)
|
||||
self.nextStep = -1
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
self.app.sweep_start()
|
||||
return
|
||||
|
@ -860,21 +933,24 @@ class CalibrationWindow(QtWidgets.QWidget):
|
|||
"Calibrate complete",
|
||||
(
|
||||
"The calibration process is now complete. Press"
|
||||
" \"Apply\" to apply the calibration parameters."
|
||||
' "Apply" to apply the calibration parameters.'
|
||||
),
|
||||
QtWidgets.QMessageBox.Apply | QtWidgets.QMessageBox.Cancel)
|
||||
QtWidgets.QMessageBox.Apply | QtWidgets.QMessageBox.Cancel,
|
||||
)
|
||||
|
||||
response = apply_step.exec()
|
||||
if response != QtWidgets.QMessageBox.Apply:
|
||||
self.btn_automatic.setDisabled(False)
|
||||
self.nextStep = -1
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
|
||||
self.calculate()
|
||||
self.btn_automatic.setDisabled(False)
|
||||
self.nextStep = -1
|
||||
self.app.worker.signals.finished.disconnect(
|
||||
self.automaticCalibrationStep)
|
||||
self.automaticCalibrationStep
|
||||
)
|
||||
return
|
||||
|
|
|
@ -23,7 +23,9 @@ from PyQt5 import QtWidgets
|
|||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def make_scrollable(window: QtWidgets.QWidget, layout: QtWidgets.QLayout) -> None:
|
||||
def make_scrollable(
|
||||
window: QtWidgets.QWidget, layout: QtWidgets.QLayout
|
||||
) -> None:
|
||||
area = QtWidgets.QScrollArea()
|
||||
area.setWidgetResizable(True)
|
||||
outer = QtWidgets.QVBoxLayout()
|
||||
|
|
|
@ -65,9 +65,11 @@ class DeviceSettingsWindow(QtWidgets.QWidget):
|
|||
settings_layout = QtWidgets.QFormLayout(settings_box)
|
||||
|
||||
self.chkValidateInputData = QtWidgets.QCheckBox(
|
||||
"Validate received data")
|
||||
"Validate received data"
|
||||
)
|
||||
validate_input = self.app.settings.value(
|
||||
"SerialInputValidation", False, bool)
|
||||
"SerialInputValidation", False, bool
|
||||
)
|
||||
self.chkValidateInputData.setChecked(validate_input)
|
||||
self.chkValidateInputData.stateChanged.connect(self.updateValidation)
|
||||
settings_layout.addRow("Validation", self.chkValidateInputData)
|
||||
|
@ -100,12 +102,10 @@ class DeviceSettingsWindow(QtWidgets.QWidget):
|
|||
settings_layout.addRow(form_layout)
|
||||
|
||||
def _set_datapoint_index(self, dpoints: int):
|
||||
self.datapoints.setCurrentIndex(
|
||||
self.datapoints.findText(str(dpoints)))
|
||||
self.datapoints.setCurrentIndex(self.datapoints.findText(str(dpoints)))
|
||||
|
||||
def _set_bandwidth_index(self, bw: int):
|
||||
self.bandwidth.setCurrentIndex(
|
||||
self.bandwidth.findText(str(bw)))
|
||||
self.bandwidth.setCurrentIndex(self.bandwidth.findText(str(bw)))
|
||||
|
||||
def show(self):
|
||||
super().show()
|
||||
|
@ -120,10 +120,10 @@ class DeviceSettingsWindow(QtWidgets.QWidget):
|
|||
self.btnCaptureScreenshot.setDisabled(True)
|
||||
return
|
||||
|
||||
self.label["status"].setText(
|
||||
f"Connected to {self.app.vna.name}.")
|
||||
self.label["status"].setText(f"Connected to {self.app.vna.name}.")
|
||||
self.label["firmware"].setText(
|
||||
f"{self.app.vna.name} v{self.app.vna.version}")
|
||||
f"{self.app.vna.name} v{self.app.vna.version}"
|
||||
)
|
||||
if self.app.worker.running:
|
||||
self.label["calibration"].setText("(Sweep running)")
|
||||
else:
|
||||
|
|
|
@ -22,8 +22,7 @@ from typing import List
|
|||
from PyQt5 import QtWidgets, QtCore, QtGui
|
||||
|
||||
from NanoVNASaver import Defaults
|
||||
from NanoVNASaver.Charts.Chart import (
|
||||
Chart, ChartColors)
|
||||
from NanoVNASaver.Charts.Chart import Chart, ChartColors
|
||||
from NanoVNASaver.Windows.Bands import BandsWindow
|
||||
from NanoVNASaver.Windows.Defaults import make_scrollable
|
||||
from NanoVNASaver.Windows.MarkerSettings import MarkerSettingsWindow
|
||||
|
@ -60,20 +59,24 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
self.returnloss_group.addButton(self.returnloss_is_negative)
|
||||
|
||||
display_options_layout.addRow(
|
||||
"Return loss is:", self.returnloss_is_negative)
|
||||
"Return loss is:", self.returnloss_is_negative
|
||||
)
|
||||
display_options_layout.addRow("", self.returnloss_is_positive)
|
||||
|
||||
self.returnloss_is_positive.setChecked(
|
||||
Defaults.cfg.chart.returnloss_is_positive)
|
||||
Defaults.cfg.chart.returnloss_is_positive
|
||||
)
|
||||
self.returnloss_is_negative.setChecked(
|
||||
not Defaults.cfg.chart.returnloss_is_positive)
|
||||
not Defaults.cfg.chart.returnloss_is_positive
|
||||
)
|
||||
|
||||
self.returnloss_is_positive.toggled.connect(self.changeReturnLoss)
|
||||
self.changeReturnLoss()
|
||||
|
||||
self.show_lines_option = QtWidgets.QCheckBox("Show lines")
|
||||
show_lines_label = QtWidgets.QLabel(
|
||||
"Displays a thin line between data points")
|
||||
"Displays a thin line between data points"
|
||||
)
|
||||
self.show_lines_option.stateChanged.connect(self.changeShowLines)
|
||||
display_options_layout.addRow(self.show_lines_option, show_lines_label)
|
||||
|
||||
|
@ -106,8 +109,7 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
self.lineThicknessInput.setSuffix(" px")
|
||||
self.lineThicknessInput.setAlignment(QtCore.Qt.AlignRight)
|
||||
self.lineThicknessInput.valueChanged.connect(self.changeLineThickness)
|
||||
display_options_layout.addRow(
|
||||
"Line thickness", self.lineThicknessInput)
|
||||
display_options_layout.addRow("Line thickness", self.lineThicknessInput)
|
||||
|
||||
self.markerSizeInput = QtWidgets.QSpinBox()
|
||||
self.markerSizeInput.setMinimumHeight(20)
|
||||
|
@ -122,25 +124,31 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
display_options_layout.addRow("Marker size", self.markerSizeInput)
|
||||
|
||||
self.show_marker_number_option = QtWidgets.QCheckBox(
|
||||
"Show marker numbers")
|
||||
"Show marker numbers"
|
||||
)
|
||||
show_marker_number_label = QtWidgets.QLabel(
|
||||
"Displays the marker number next to the marker")
|
||||
"Displays the marker number next to the marker"
|
||||
)
|
||||
self.show_marker_number_option.stateChanged.connect(
|
||||
self.changeShowMarkerNumber)
|
||||
self.changeShowMarkerNumber
|
||||
)
|
||||
display_options_layout.addRow(
|
||||
self.show_marker_number_option, show_marker_number_label)
|
||||
self.show_marker_number_option, show_marker_number_label
|
||||
)
|
||||
|
||||
self.filled_marker_option = QtWidgets.QCheckBox("Filled markers")
|
||||
filled_marker_label = QtWidgets.QLabel(
|
||||
"Shows the marker as a filled triangle")
|
||||
self.filled_marker_option.stateChanged.connect(
|
||||
self.changeFilledMarkers)
|
||||
"Shows the marker as a filled triangle"
|
||||
)
|
||||
self.filled_marker_option.stateChanged.connect(self.changeFilledMarkers)
|
||||
display_options_layout.addRow(
|
||||
self.filled_marker_option, filled_marker_label)
|
||||
self.filled_marker_option, filled_marker_label
|
||||
)
|
||||
|
||||
self.marker_tip_group = QtWidgets.QButtonGroup()
|
||||
self.marker_at_center = QtWidgets.QRadioButton(
|
||||
"At the center of the marker")
|
||||
"At the center of the marker"
|
||||
)
|
||||
self.marker_at_tip = QtWidgets.QRadioButton("At the tip of the marker")
|
||||
self.marker_tip_group.addButton(self.marker_at_center)
|
||||
self.marker_tip_group.addButton(self.marker_at_tip)
|
||||
|
@ -183,11 +191,12 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
self.show_bands = QtWidgets.QCheckBox("Show bands")
|
||||
self.show_bands.setChecked(self.app.bands.enabled)
|
||||
self.show_bands.stateChanged.connect(
|
||||
lambda: self.setShowBands(self.show_bands.isChecked()))
|
||||
lambda: self.setShowBands(self.show_bands.isChecked())
|
||||
)
|
||||
bands_layout.addRow(self.show_bands)
|
||||
bands_layout.addRow(
|
||||
"Chart bands",
|
||||
self.color_picker("BandsColor", "bands"))
|
||||
"Chart bands", self.color_picker("BandsColor", "bands")
|
||||
)
|
||||
|
||||
self.btn_manage_bands = QtWidgets.QPushButton("Manage bands")
|
||||
self.btn_manage_bands.setMinimumHeight(20)
|
||||
|
@ -201,16 +210,19 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
vswr_marker_layout = QtWidgets.QFormLayout(vswr_marker_box)
|
||||
|
||||
self.vswrMarkers: List[float] = self.app.settings.value(
|
||||
"VSWRMarkers", [], float)
|
||||
"VSWRMarkers", [], float
|
||||
)
|
||||
|
||||
if isinstance(self.vswrMarkers, float):
|
||||
# Single values from the .ini become floats rather than lists.
|
||||
# Convert them.
|
||||
self.vswrMarkers = ([] if self.vswrMarkers == 0.0 else
|
||||
[self.vswrMarkers])
|
||||
self.vswrMarkers = (
|
||||
[] if self.vswrMarkers == 0.0 else [self.vswrMarkers]
|
||||
)
|
||||
|
||||
vswr_marker_layout.addRow(
|
||||
"VSWR Markers", self.color_picker("VSWRColor", "swr"))
|
||||
"VSWR Markers", self.color_picker("VSWRColor", "swr")
|
||||
)
|
||||
|
||||
self.vswr_marker_dropdown = QtWidgets.QComboBox()
|
||||
self.vswr_marker_dropdown.setMinimumHeight(20)
|
||||
|
@ -281,7 +293,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
chart00_selection.setCurrentText("S11 Smith Chart")
|
||||
chart00_selection.currentTextChanged.connect(
|
||||
lambda: self.changeChart(0, 0, chart00_selection.currentText()))
|
||||
lambda: self.changeChart(0, 0, chart00_selection.currentText())
|
||||
)
|
||||
charts_layout.addWidget(chart00_selection, 0, 0)
|
||||
|
||||
chart01_selection = QtWidgets.QComboBox()
|
||||
|
@ -293,7 +306,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
chart01_selection.setCurrentText("S11 Return Loss")
|
||||
chart01_selection.currentTextChanged.connect(
|
||||
lambda: self.changeChart(0, 1, chart01_selection.currentText()))
|
||||
lambda: self.changeChart(0, 1, chart01_selection.currentText())
|
||||
)
|
||||
charts_layout.addWidget(chart01_selection, 0, 1)
|
||||
|
||||
chart02_selection = QtWidgets.QComboBox()
|
||||
|
@ -305,7 +319,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
chart02_selection.setCurrentText("None")
|
||||
chart02_selection.currentTextChanged.connect(
|
||||
lambda: self.changeChart(0, 2, chart02_selection.currentText()))
|
||||
lambda: self.changeChart(0, 2, chart02_selection.currentText())
|
||||
)
|
||||
charts_layout.addWidget(chart02_selection, 0, 2)
|
||||
|
||||
chart10_selection = QtWidgets.QComboBox()
|
||||
|
@ -317,7 +332,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
chart10_selection.setCurrentText("S21 Polar Plot")
|
||||
chart10_selection.currentTextChanged.connect(
|
||||
lambda: self.changeChart(1, 0, chart10_selection.currentText()))
|
||||
lambda: self.changeChart(1, 0, chart10_selection.currentText())
|
||||
)
|
||||
charts_layout.addWidget(chart10_selection, 1, 0)
|
||||
|
||||
chart11_selection = QtWidgets.QComboBox()
|
||||
|
@ -329,7 +345,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
chart11_selection.setCurrentText("S21 Gain")
|
||||
chart11_selection.currentTextChanged.connect(
|
||||
lambda: self.changeChart(1, 1, chart11_selection.currentText()))
|
||||
lambda: self.changeChart(1, 1, chart11_selection.currentText())
|
||||
)
|
||||
charts_layout.addWidget(chart11_selection, 1, 1)
|
||||
|
||||
chart12_selection = QtWidgets.QComboBox()
|
||||
|
@ -341,7 +358,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
chart12_selection.setCurrentText("None")
|
||||
chart12_selection.currentTextChanged.connect(
|
||||
lambda: self.changeChart(1, 2, chart12_selection.currentText()))
|
||||
lambda: self.changeChart(1, 2, chart12_selection.currentText())
|
||||
)
|
||||
charts_layout.addWidget(chart12_selection, 1, 2)
|
||||
|
||||
self.changeChart(0, 0, chart00_selection.currentText())
|
||||
|
@ -353,30 +371,36 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
chart_colors = ChartColors()
|
||||
Chart.color.background = self.app.settings.value(
|
||||
"BackgroundColor", defaultValue=chart_colors.background,
|
||||
type=QtGui.QColor)
|
||||
"BackgroundColor",
|
||||
defaultValue=chart_colors.background,
|
||||
type=QtGui.QColor,
|
||||
)
|
||||
Chart.color.foreground = self.app.settings.value(
|
||||
"ForegroundColor", defaultValue=chart_colors.foreground,
|
||||
type=QtGui.QColor)
|
||||
"ForegroundColor",
|
||||
defaultValue=chart_colors.foreground,
|
||||
type=QtGui.QColor,
|
||||
)
|
||||
Chart.color.text = self.app.settings.value(
|
||||
"TextColor", defaultValue=chart_colors.text,
|
||||
type=QtGui.QColor)
|
||||
"TextColor", defaultValue=chart_colors.text, type=QtGui.QColor
|
||||
)
|
||||
self.bandsColor = self.app.settings.value(
|
||||
"BandsColor", defaultValue=chart_colors.bands,
|
||||
type=QtGui.QColor)
|
||||
"BandsColor", defaultValue=chart_colors.bands, type=QtGui.QColor
|
||||
)
|
||||
self.app.bands.color = Chart.color.bands
|
||||
Chart.color.swr = self.app.settings.value(
|
||||
"VSWRColor", defaultValue=chart_colors.swr,
|
||||
type=QtGui.QColor)
|
||||
"VSWRColor", defaultValue=chart_colors.swr, type=QtGui.QColor
|
||||
)
|
||||
|
||||
self.dark_mode_option.setChecked(Defaults.cfg.gui.dark_mode)
|
||||
self.show_lines_option.setChecked(Defaults.cfg.chart.show_lines)
|
||||
self.show_marker_number_option.setChecked(
|
||||
Defaults.cfg.chart.marker_label)
|
||||
Defaults.cfg.chart.marker_label
|
||||
)
|
||||
self.filled_marker_option.setChecked(Defaults.cfg.chart.marker_filled)
|
||||
|
||||
if self.app.settings.value("UseCustomColors",
|
||||
defaultValue=False, type=bool):
|
||||
if self.app.settings.value(
|
||||
"UseCustomColors", defaultValue=False, type=bool
|
||||
):
|
||||
self.dark_mode_option.setDisabled(True)
|
||||
self.dark_mode_option.setChecked(False)
|
||||
self.use_custom_colors.setChecked(True)
|
||||
|
@ -395,20 +419,23 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
def trace_colors(self, layout: QtWidgets.QLayout):
|
||||
for setting, name, attr in (
|
||||
('SweepColor', 'Sweep color', 'sweep'),
|
||||
('SecondarySweepColor', 'Second sweep color', 'sweep_secondary'),
|
||||
('ReferenceColor', 'Reference color', 'reference'),
|
||||
('SecondaryReferenceColor',
|
||||
'Second reference color', 'reference_secondary'),
|
||||
("SweepColor", "Sweep color", "sweep"),
|
||||
("SecondarySweepColor", "Second sweep color", "sweep_secondary"),
|
||||
("ReferenceColor", "Reference color", "reference"),
|
||||
(
|
||||
"SecondaryReferenceColor",
|
||||
"Second reference color",
|
||||
"reference_secondary",
|
||||
),
|
||||
):
|
||||
cp = self.color_picker(setting, attr)
|
||||
layout.addRow(name, cp)
|
||||
|
||||
def custom_colors(self, layout: QtWidgets.QLayout):
|
||||
for setting, name, attr in (
|
||||
('BackgroundColor', 'Chart background', 'background'),
|
||||
('ForegroundColor', 'Chart foreground', 'foreground'),
|
||||
('TextColor', 'Chart text', 'text'),
|
||||
("BackgroundColor", "Chart background", "background"),
|
||||
("ForegroundColor", "Chart foreground", "foreground"),
|
||||
("TextColor", "Chart text", "text"),
|
||||
):
|
||||
cp = self.color_picker(setting, attr)
|
||||
layout.addRow(name, cp)
|
||||
|
@ -419,7 +446,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
cp.setMinimumHeight(20)
|
||||
default = getattr(Chart.color, attr)
|
||||
color = self.app.settings.value(
|
||||
setting, defaultValue=default, type=QtGui.QColor)
|
||||
setting, defaultValue=default, type=QtGui.QColor
|
||||
)
|
||||
setattr(Chart.color, attr, color)
|
||||
self.callback_params[cp] = (setting, attr)
|
||||
cp.clicked.connect(self.setColor)
|
||||
|
@ -466,17 +494,18 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
def changeShowMarkerNumber(self):
|
||||
Defaults.cfg.chart.marker_label = bool(
|
||||
self.show_marker_number_option.isChecked())
|
||||
self.show_marker_number_option.isChecked()
|
||||
)
|
||||
self.updateCharts()
|
||||
|
||||
def changeFilledMarkers(self):
|
||||
Defaults.cfg.chart.marker_filled = bool(
|
||||
self.filled_marker_option.isChecked())
|
||||
self.filled_marker_option.isChecked()
|
||||
)
|
||||
self.updateCharts()
|
||||
|
||||
def changeMarkerAtTip(self):
|
||||
Defaults.cfg.chart.marker_at_tip = bool(
|
||||
self.marker_at_tip.isChecked())
|
||||
Defaults.cfg.chart.marker_at_tip = bool(self.marker_at_tip.isChecked())
|
||||
self.updateCharts()
|
||||
|
||||
def changePointSize(self, size: int):
|
||||
|
@ -521,7 +550,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
color = getattr(Chart.color, attr)
|
||||
color = QtWidgets.QColorDialog.getColor(
|
||||
color, options=QtWidgets.QColorDialog.ShowAlphaChannel)
|
||||
color, options=QtWidgets.QColorDialog.ShowAlphaChannel
|
||||
)
|
||||
|
||||
if not color.isValid():
|
||||
logger.info("Invalid color")
|
||||
|
@ -566,7 +596,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
new_marker.updated.connect(self.app.markerUpdated)
|
||||
label, layout = new_marker.getRow()
|
||||
self.app.marker_control.layout.insertRow(
|
||||
Marker.count() - 1, label, layout)
|
||||
Marker.count() - 1, label, layout
|
||||
)
|
||||
self.btn_remove_marker.setDisabled(False)
|
||||
|
||||
if Marker.count() >= 2:
|
||||
|
@ -594,8 +625,12 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
def addVSWRMarker(self):
|
||||
value, selected = QtWidgets.QInputDialog.getDouble(
|
||||
self, "Add VSWR Marker", "VSWR value to show:",
|
||||
min=1.001, decimals=3)
|
||||
self,
|
||||
"Add VSWR Marker",
|
||||
"VSWR value to show:",
|
||||
min=1.001,
|
||||
decimals=3,
|
||||
)
|
||||
if selected:
|
||||
self.vswrMarkers.append(value)
|
||||
if self.vswr_marker_dropdown.itemText(0) == "None":
|
||||
|
@ -612,7 +647,8 @@ class DisplaySettingsWindow(QtWidgets.QWidget):
|
|||
value = float(value_str)
|
||||
self.vswrMarkers.remove(value)
|
||||
self.vswr_marker_dropdown.removeItem(
|
||||
self.vswr_marker_dropdown.currentIndex())
|
||||
self.vswr_marker_dropdown.currentIndex()
|
||||
)
|
||||
if self.vswr_marker_dropdown.count() == 0:
|
||||
self.vswr_marker_dropdown.addItem("None")
|
||||
self.app.settings.remove("VSWRMarkers")
|
||||
|
|
|
@ -68,27 +68,32 @@ class FilesWindow(QtWidgets.QWidget):
|
|||
|
||||
btn_open_file_window = QtWidgets.QPushButton("Files ...")
|
||||
btn_open_file_window.clicked.connect(
|
||||
lambda: self.app.display_window("file"))
|
||||
lambda: self.app.display_window("file")
|
||||
)
|
||||
|
||||
def exportFile(self, nr_params: int = 1):
|
||||
if len(self.app.data.s11) == 0:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "No data to save", "There is no data to save.")
|
||||
self, "No data to save", "There is no data to save."
|
||||
)
|
||||
return
|
||||
if nr_params > 2 and len(self.app.data.s21) == 0:
|
||||
QtWidgets.QMessageBox.warning(
|
||||
self, "No S21 data to save", "There is no S21 data to save.")
|
||||
self, "No S21 data to save", "There is no S21 data to save."
|
||||
)
|
||||
return
|
||||
|
||||
filedialog = QtWidgets.QFileDialog(self)
|
||||
if nr_params == 1:
|
||||
filedialog.setDefaultSuffix("s1p")
|
||||
filedialog.setNameFilter(
|
||||
"Touchstone 1-Port Files (*.s1p);;All files (*.*)")
|
||||
"Touchstone 1-Port Files (*.s1p);;All files (*.*)"
|
||||
)
|
||||
else:
|
||||
filedialog.setDefaultSuffix("s2p")
|
||||
filedialog.setNameFilter(
|
||||
"Touchstone 2-Port Files (*.s2p);;All files (*.*)")
|
||||
"Touchstone 2-Port Files (*.s2p);;All files (*.*)"
|
||||
)
|
||||
filedialog.setAcceptMode(QtWidgets.QFileDialog.AcceptSave)
|
||||
selected = filedialog.exec()
|
||||
if not selected:
|
||||
|
@ -113,7 +118,8 @@ class FilesWindow(QtWidgets.QWidget):
|
|||
|
||||
def loadReferenceFile(self):
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)"
|
||||
)
|
||||
if filename != "":
|
||||
self.app.resetReference()
|
||||
t = Touchstone(filename)
|
||||
|
@ -122,7 +128,8 @@ class FilesWindow(QtWidgets.QWidget):
|
|||
|
||||
def loadSweepFile(self):
|
||||
filename, _ = QtWidgets.QFileDialog.getOpenFileName(
|
||||
filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)")
|
||||
filter="Touchstone Files (*.s1p *.s2p);;All files (*.*)"
|
||||
)
|
||||
if filename != "":
|
||||
self.app.data.s11 = []
|
||||
self.app.data.s21 = []
|
||||
|
|
|
@ -28,12 +28,16 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class MarkerSettingsWindow(QtWidgets.QWidget):
|
||||
exampleData11 = [Datapoint(123000000, 0.89, -0.11),
|
||||
Datapoint(123500000, 0.9, -0.1),
|
||||
Datapoint(124000000, 0.91, -0.95)]
|
||||
exampleData21 = [Datapoint(123000000, -0.25, 0.49),
|
||||
Datapoint(123456000, -0.3, 0.5),
|
||||
Datapoint(124000000, -0.2, 0.5)]
|
||||
exampleData11 = [
|
||||
Datapoint(123000000, 0.89, -0.11),
|
||||
Datapoint(123500000, 0.9, -0.1),
|
||||
Datapoint(124000000, 0.91, -0.95),
|
||||
]
|
||||
exampleData21 = [
|
||||
Datapoint(123000000, -0.25, 0.49),
|
||||
Datapoint(123456000, -0.3, 0.5),
|
||||
Datapoint(124000000, -0.2, 0.5),
|
||||
]
|
||||
|
||||
def __init__(self, app: QtWidgets.QWidget):
|
||||
super().__init__()
|
||||
|
@ -50,10 +54,10 @@ class MarkerSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
settings_group_box = QtWidgets.QGroupBox("Settings")
|
||||
settings_group_box_layout = QtWidgets.QFormLayout(settings_group_box)
|
||||
self.checkboxColouredMarker = QtWidgets.QCheckBox(
|
||||
"Colored marker name")
|
||||
self.checkboxColouredMarker = QtWidgets.QCheckBox("Colored marker name")
|
||||
self.checkboxColouredMarker.setChecked(
|
||||
self.app.settings.value("ColoredMarkerNames", True, bool))
|
||||
self.app.settings.value("ColoredMarkerNames", True, bool)
|
||||
)
|
||||
self.checkboxColouredMarker.stateChanged.connect(self.updateMarker)
|
||||
settings_group_box_layout.addRow(self.checkboxColouredMarker)
|
||||
|
||||
|
@ -103,7 +107,8 @@ class MarkerSettingsWindow(QtWidgets.QWidget):
|
|||
def updateMarker(self):
|
||||
self.exampleMarker.setFrequency(123456000)
|
||||
self.exampleMarker.setColoredText(
|
||||
self.checkboxColouredMarker.isChecked())
|
||||
self.checkboxColouredMarker.isChecked()
|
||||
)
|
||||
self.exampleMarker.setFieldSelection(self.currentFieldSelection)
|
||||
self.exampleMarker.findLocation(self.exampleData11)
|
||||
self.exampleMarker.resetLabels()
|
||||
|
@ -125,8 +130,11 @@ class MarkerSettingsWindow(QtWidgets.QWidget):
|
|||
self.savedFieldSelection = self.currentFieldSelection[:]
|
||||
self.app.settings.setValue("MarkerFields", self.savedFieldSelection)
|
||||
self.app.settings.setValue(
|
||||
"ColoredMarkerNames", self.checkboxColouredMarker.isChecked())
|
||||
for m in self.app.markers + [self.app.delta_marker, ]:
|
||||
"ColoredMarkerNames", self.checkboxColouredMarker.isChecked()
|
||||
)
|
||||
for m in self.app.markers + [
|
||||
self.app.delta_marker,
|
||||
]:
|
||||
m.setFieldSelection(self.savedFieldSelection)
|
||||
m.setColoredText(self.checkboxColouredMarker.isChecked())
|
||||
|
||||
|
|
|
@ -61,25 +61,34 @@ class ScreenshotWindow(QtWidgets.QLabel):
|
|||
self.pix.scaled(
|
||||
self.size(),
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.FastTransformation))
|
||||
QtCore.Qt.FastTransformation,
|
||||
)
|
||||
)
|
||||
w, h = pixmap.width(), pixmap.height()
|
||||
self.action_original_size.setText(
|
||||
"Original size (" + str(w) + "x" + str(h) + ")")
|
||||
"Original size (" + str(w) + "x" + str(h) + ")"
|
||||
)
|
||||
self.action_2x_size.setText(
|
||||
"2x size (" + str(w * 2) + "x" + str(h * 2) + ")")
|
||||
"2x size (" + str(w * 2) + "x" + str(h * 2) + ")"
|
||||
)
|
||||
self.action_3x_size.setText(
|
||||
"3x size (" + str(w * 3) + "x" + str(h * 3) + ")")
|
||||
"3x size (" + str(w * 3) + "x" + str(h * 3) + ")"
|
||||
)
|
||||
self.action_4x_size.setText(
|
||||
"4x size (" + str(w * 4) + "x" + str(h * 4) + ")")
|
||||
"4x size (" + str(w * 4) + "x" + str(h * 4) + ")"
|
||||
)
|
||||
self.action_5x_size.setText(
|
||||
"5x size (" + str(w * 5) + "x" + str(h * 5) + ")")
|
||||
"5x size (" + str(w * 5) + "x" + str(h * 5) + ")"
|
||||
)
|
||||
|
||||
def saveScreenshot(self):
|
||||
if self.pix is not None:
|
||||
logger.info("Saving screenshot to file...")
|
||||
filename, _ = QtWidgets.QFileDialog.getSaveFileName(
|
||||
parent=self, caption="Save image",
|
||||
filter="PNG (*.png);;All files (*.*)")
|
||||
parent=self,
|
||||
caption="Save image",
|
||||
filter="PNG (*.png);;All files (*.*)",
|
||||
)
|
||||
|
||||
logger.debug("Filename: %s", filename)
|
||||
if filename != "":
|
||||
|
@ -94,9 +103,13 @@ class ScreenshotWindow(QtWidgets.QLabel):
|
|||
self.pix.scaled(
|
||||
self.size(),
|
||||
QtCore.Qt.KeepAspectRatio,
|
||||
QtCore.Qt.FastTransformation))
|
||||
QtCore.Qt.FastTransformation,
|
||||
)
|
||||
)
|
||||
|
||||
def setScale(self, scale):
|
||||
width, height = (self.pix.size().width() * scale,
|
||||
self.pix.size().height() * scale)
|
||||
width, height = (
|
||||
self.pix.size().width() * scale,
|
||||
self.pix.size().height() * scale,
|
||||
)
|
||||
self.resize(width, height)
|
||||
|
|
|
@ -21,7 +21,8 @@ from functools import partial
|
|||
from PyQt5 import QtWidgets, QtCore
|
||||
|
||||
from NanoVNASaver.Formatting import (
|
||||
format_frequency_short, format_frequency_sweep,
|
||||
format_frequency_short,
|
||||
format_frequency_sweep,
|
||||
)
|
||||
from NanoVNASaver.Settings.Sweep import SweepMode
|
||||
from NanoVNASaver.Windows.Defaults import make_scrollable
|
||||
|
@ -59,11 +60,12 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
input_title = QtWidgets.QLineEdit(self.app.sweep.properties.name)
|
||||
input_title.setMinimumHeight(20)
|
||||
input_title.editingFinished.connect(
|
||||
lambda: self.update_title(input_title.text()))
|
||||
lambda: self.update_title(input_title.text())
|
||||
)
|
||||
layout.addRow(input_title)
|
||||
return box
|
||||
|
||||
def settings_box(self) -> 'QtWidgets.QWidget':
|
||||
def settings_box(self) -> "QtWidgets.QWidget":
|
||||
box = QtWidgets.QGroupBox("Settings")
|
||||
layout = QtWidgets.QFormLayout(box)
|
||||
|
||||
|
@ -73,25 +75,29 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
radio_button = QtWidgets.QRadioButton("Single sweep")
|
||||
radio_button.setMinimumHeight(20)
|
||||
radio_button.setChecked(
|
||||
self.app.sweep.properties.mode == SweepMode.SINGLE)
|
||||
radio_button.clicked.connect(
|
||||
lambda: self.update_mode(SweepMode.SINGLE))
|
||||
self.app.sweep.properties.mode == SweepMode.SINGLE
|
||||
)
|
||||
radio_button.clicked.connect(lambda: self.update_mode(SweepMode.SINGLE))
|
||||
sweep_btn_layout.addWidget(radio_button)
|
||||
|
||||
radio_button = QtWidgets.QRadioButton("Continous sweep")
|
||||
radio_button.setMinimumHeight(20)
|
||||
radio_button.setChecked(
|
||||
self.app.sweep.properties.mode == SweepMode.CONTINOUS)
|
||||
self.app.sweep.properties.mode == SweepMode.CONTINOUS
|
||||
)
|
||||
radio_button.clicked.connect(
|
||||
lambda: self.update_mode(SweepMode.CONTINOUS))
|
||||
lambda: self.update_mode(SweepMode.CONTINOUS)
|
||||
)
|
||||
sweep_btn_layout.addWidget(radio_button)
|
||||
|
||||
radio_button = QtWidgets.QRadioButton("Averaged sweep")
|
||||
radio_button.setMinimumHeight(20)
|
||||
radio_button.setChecked(
|
||||
self.app.sweep.properties.mode == SweepMode.AVERAGE)
|
||||
self.app.sweep.properties.mode == SweepMode.AVERAGE
|
||||
)
|
||||
radio_button.clicked.connect(
|
||||
lambda: self.update_mode(SweepMode.AVERAGE))
|
||||
lambda: self.update_mode(SweepMode.AVERAGE)
|
||||
)
|
||||
sweep_btn_layout.addWidget(radio_button)
|
||||
|
||||
layout.addRow(sweep_btn_layout)
|
||||
|
@ -101,7 +107,8 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
"Logarithmic sweeping changes the step width in each segment"
|
||||
" in logarithmical manner. Useful in conjunction with small"
|
||||
" amount of datapoints and many segments. Step display in"
|
||||
" SweepControl cannot reflect this currently.")
|
||||
" SweepControl cannot reflect this currently."
|
||||
)
|
||||
label.setWordWrap(True)
|
||||
label.setMinimumSize(600, 70)
|
||||
layout.addRow(label)
|
||||
|
@ -109,26 +116,32 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
checkbox.setMinimumHeight(20)
|
||||
checkbox.setCheckState(self.app.sweep.properties.logarithmic)
|
||||
checkbox.toggled.connect(
|
||||
lambda: self.update_logarithmic(checkbox.isChecked()))
|
||||
lambda: self.update_logarithmic(checkbox.isChecked())
|
||||
)
|
||||
layout.addRow(checkbox)
|
||||
|
||||
# Averaging
|
||||
label = QtWidgets.QLabel(
|
||||
"Averaging allows discarding outlying samples to get better"
|
||||
" averages. Common values are 3/0, 5/2, 9/4 and 25/6.")
|
||||
" averages. Common values are 3/0, 5/2, 9/4 and 25/6."
|
||||
)
|
||||
label.setWordWrap(True)
|
||||
label.setMinimumHeight(50)
|
||||
layout.addRow(label)
|
||||
averages = QtWidgets.QLineEdit(
|
||||
str(self.app.sweep.properties.averages[0]))
|
||||
str(self.app.sweep.properties.averages[0])
|
||||
)
|
||||
averages.setMinimumHeight(20)
|
||||
truncates = QtWidgets.QLineEdit(
|
||||
str(self.app.sweep.properties.averages[1]))
|
||||
str(self.app.sweep.properties.averages[1])
|
||||
)
|
||||
truncates.setMinimumHeight(20)
|
||||
averages.editingFinished.connect(
|
||||
lambda: self.update_averaging(averages, truncates))
|
||||
lambda: self.update_averaging(averages, truncates)
|
||||
)
|
||||
truncates.editingFinished.connect(
|
||||
lambda: self.update_averaging(averages, truncates))
|
||||
lambda: self.update_averaging(averages, truncates)
|
||||
)
|
||||
layout.addRow("Number of measurements to average", averages)
|
||||
layout.addRow("Number to discard", truncates)
|
||||
|
||||
|
@ -136,7 +149,8 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
label = QtWidgets.QLabel(
|
||||
"Some times when you measure amplifiers you need to use an"
|
||||
" attenuator in line with the S21 input (CH1) here you can"
|
||||
" specify it.")
|
||||
" specify it."
|
||||
)
|
||||
label.setWordWrap(True)
|
||||
label.setMinimumHeight(50)
|
||||
layout.addRow(label)
|
||||
|
@ -144,11 +158,12 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
input_att = QtWidgets.QLineEdit(str(self.app.s21att))
|
||||
input_att.setMinimumHeight(20)
|
||||
input_att.editingFinished.connect(
|
||||
lambda: self.update_attenuator(input_att))
|
||||
lambda: self.update_attenuator(input_att)
|
||||
)
|
||||
layout.addRow("Attenuator in port CH1 (s21) in dB", input_att)
|
||||
return box
|
||||
|
||||
def sweep_box(self) -> 'QtWidgets.QWidget':
|
||||
def sweep_box(self) -> "QtWidgets.QWidget":
|
||||
box = QtWidgets.QGroupBox("Sweep band")
|
||||
layout = QtWidgets.QFormLayout(box)
|
||||
sweep_pad_layout = QtWidgets.QHBoxLayout()
|
||||
|
@ -162,7 +177,11 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
sweep_pad_layout.addWidget(QtWidgets.QLabel("Pad band limits:"))
|
||||
for btn_label, value in (
|
||||
("None", 0), ("10%", 10), ("25%", 25), ("100%", 100),):
|
||||
("None", 0),
|
||||
("10%", 10),
|
||||
("25%", 25),
|
||||
("100%", 100),
|
||||
):
|
||||
radio_button = QtWidgets.QRadioButton(btn_label)
|
||||
radio_button.setMinimumHeight(20)
|
||||
radio_button.setChecked(self.padding == value)
|
||||
|
@ -186,20 +205,33 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
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)
|
||||
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)
|
||||
self.band_list.currentIndex(), 1
|
||||
)
|
||||
index_stop = self.band_list.model().index(
|
||||
self.band_list.currentIndex(), 2)
|
||||
start = int(self.band_list.model().data(
|
||||
index_start, QtCore.Qt.ItemDataRole).value())
|
||||
stop = int(self.band_list.model().data(
|
||||
index_stop, QtCore.Qt.ItemDataRole).value())
|
||||
self.band_list.currentIndex(), 2
|
||||
)
|
||||
start = int(
|
||||
self.band_list.model()
|
||||
.data(index_start, QtCore.Qt.ItemDataRole)
|
||||
.value()
|
||||
)
|
||||
stop = int(
|
||||
self.band_list.model()
|
||||
.data(index_stop, QtCore.Qt.ItemDataRole)
|
||||
.value()
|
||||
)
|
||||
|
||||
if self.padding > 0:
|
||||
span = stop - start
|
||||
|
@ -209,33 +241,37 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
|
||||
self.band_label.setText(
|
||||
f"Sweep span: {format_frequency_short(start)}"
|
||||
f" to {format_frequency_short(stop)}")
|
||||
f" to {format_frequency_short(stop)}"
|
||||
)
|
||||
|
||||
if not apply:
|
||||
return
|
||||
|
||||
self.app.sweep_control.input_start.setText(
|
||||
format_frequency_sweep(start))
|
||||
self.app.sweep_control.input_end.setText(
|
||||
format_frequency_sweep(stop))
|
||||
format_frequency_sweep(start)
|
||||
)
|
||||
self.app.sweep_control.input_end.setText(format_frequency_sweep(stop))
|
||||
self.app.sweep_control.input_end.textEdited.emit(
|
||||
self.app.sweep_control.input_end.text())
|
||||
self.app.sweep_control.input_end.text()
|
||||
)
|
||||
|
||||
def update_attenuator(self, value: 'QtWidgets.QLineEdit'):
|
||||
def update_attenuator(self, value: "QtWidgets.QLineEdit"):
|
||||
try:
|
||||
att = float(value.text())
|
||||
assert att >= 0
|
||||
except (ValueError, AssertionError):
|
||||
logger.warning("Values for attenuator are absolute and with no"
|
||||
" minus sign, resetting.")
|
||||
logger.warning(
|
||||
"Values for attenuator are absolute and with no"
|
||||
" minus sign, resetting."
|
||||
)
|
||||
att = 0
|
||||
logger.debug("Attenuator %sdB inline with S21 input", att)
|
||||
value.setText(str(att))
|
||||
self.app.s21att = att
|
||||
|
||||
def update_averaging(self,
|
||||
averages: 'QtWidgets.QLineEdit',
|
||||
truncs: 'QtWidgets.QLineEdit'):
|
||||
def update_averaging(
|
||||
self, averages: "QtWidgets.QLineEdit", truncs: "QtWidgets.QLineEdit"
|
||||
):
|
||||
try:
|
||||
amount = int(averages.text())
|
||||
truncates = int(truncs.text())
|
||||
|
@ -257,7 +293,7 @@ class SweepSettingsWindow(QtWidgets.QWidget):
|
|||
with self.app.sweep.lock:
|
||||
self.app.sweep.properties.logarithmic = logarithmic
|
||||
|
||||
def update_mode(self, mode: 'SweepMode'):
|
||||
def update_mode(self, mode: "SweepMode"):
|
||||
logger.debug("update_mode(%s)", mode)
|
||||
with self.app.sweep.lock:
|
||||
self.app.sweep.properties.mode = mode
|
||||
|
|
|
@ -20,6 +20,7 @@ import logging
|
|||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
# pylint: disable=import-error, no-name-in-module
|
||||
from scipy.signal import convolve
|
||||
from scipy.constants import speed_of_light
|
||||
|
@ -48,9 +49,9 @@ CABLE_PARAMETERS = (
|
|||
("RG-8/U (Shireen RFC®400 Low Loss) (0.86)", 0.86),
|
||||
("RG-8X (Belden 9258) (0.82)", 0.82),
|
||||
# Next three added by EKZ, KC3KZ, from measurement of actual cable
|
||||
("RG-8X (Wireman \"Super 8\" CQ106) (0.81)", 0.81),
|
||||
("RG-8X (Wireman \"MINI-8 Lo-Loss\" CQ118) (0.82)", 0.82),
|
||||
("RG-58 (Wireman \"CQ 58 Lo-Loss Flex\" CQ129FF) (0.79)", 0.79),
|
||||
('RG-8X (Wireman "Super 8" CQ106) (0.81)', 0.81),
|
||||
('RG-8X (Wireman "MINI-8 Lo-Loss" CQ118) (0.82)', 0.82),
|
||||
('RG-58 (Wireman "CQ 58 Lo-Loss Flex" CQ129FF) (0.79)', 0.79),
|
||||
("RG-11/U 75\N{OHM SIGN} Foam HDPE (Belden 9292) (0.84)", 0.84),
|
||||
("RG-58/U 52\N{OHM SIGN} PE (Belden 9201) (0.66)", 0.66),
|
||||
("RG-58A/U 54\N{OHM SIGN} Foam (Belden 8219) (0.73)", 0.73),
|
||||
|
@ -92,7 +93,8 @@ class TDRWindow(QtWidgets.QWidget):
|
|||
for cable_name, velocity in CABLE_PARAMETERS:
|
||||
self.tdr_velocity_dropdown.addItem(cable_name, velocity)
|
||||
self.tdr_velocity_dropdown.insertSeparator(
|
||||
self.tdr_velocity_dropdown.count())
|
||||
self.tdr_velocity_dropdown.count()
|
||||
)
|
||||
self.tdr_velocity_dropdown.addItem("Custom", -1)
|
||||
self.tdr_velocity_dropdown.setCurrentIndex(1) # Default to PE (0.66)
|
||||
self.tdr_velocity_dropdown.currentIndexChanged.connect(self.updateTDR)
|
||||
|
@ -121,7 +123,8 @@ class TDRWindow(QtWidgets.QWidget):
|
|||
else:
|
||||
self.tdr_velocity_input.setDisabled(True)
|
||||
self.tdr_velocity_input.setText(
|
||||
str(self.tdr_velocity_dropdown.currentData()))
|
||||
str(self.tdr_velocity_dropdown.currentData())
|
||||
)
|
||||
|
||||
try:
|
||||
v = float(self.tdr_velocity_input.text())
|
||||
|
@ -142,8 +145,7 @@ class TDRWindow(QtWidgets.QWidget):
|
|||
step = np.ones(FFT_POINTS)
|
||||
step_response = convolve(td, step)
|
||||
|
||||
self.step_response_Z = 50 * (
|
||||
1 + step_response) / (1 - step_response)
|
||||
self.step_response_Z = 50 * (1 + step_response) / (1 - step_response)
|
||||
|
||||
time_axis = np.linspace(0, 1 / step_size, FFT_POINTS)
|
||||
self.distance_axis = time_axis * v * speed_of_light
|
||||
|
|
|
@ -9,16 +9,17 @@ from .MarkerSettings import MarkerSettingsWindow
|
|||
from .Screenshot import ScreenshotWindow
|
||||
from .SweepSettings import SweepSettingsWindow
|
||||
from .TDR import TDRWindow
|
||||
|
||||
__all__ = [
|
||||
'AboutWindow',
|
||||
'AnalysisWindow',
|
||||
'BandsWindow',
|
||||
'CalibrationWindow',
|
||||
'DeviceSettingsWindow',
|
||||
'DisplaySettingsWindow',
|
||||
'FilesWindow',
|
||||
'MarkerSettingsWindow',
|
||||
'ScreenshotWindow',
|
||||
'SweepSettingsWindow',
|
||||
'TDRWindow',
|
||||
"AboutWindow",
|
||||
"AnalysisWindow",
|
||||
"BandsWindow",
|
||||
"CalibrationWindow",
|
||||
"DeviceSettingsWindow",
|
||||
"DisplaySettingsWindow",
|
||||
"FilesWindow",
|
||||
"MarkerSettingsWindow",
|
||||
"ScreenshotWindow",
|
||||
"SweepSettingsWindow",
|
||||
"TDRWindow",
|
||||
]
|
||||
|
|
|
@ -2,9 +2,15 @@ import sys
|
|||
|
||||
if sys.version_info[:2] >= (3, 8):
|
||||
# TODO: Import directly (no need for conditional) when `python_requires = >= 3.8`
|
||||
from importlib.metadata import PackageNotFoundError, version # pragma: no cover
|
||||
from importlib.metadata import (
|
||||
PackageNotFoundError,
|
||||
version,
|
||||
) # pragma: no cover
|
||||
else:
|
||||
from importlib_metadata import PackageNotFoundError, version # pragma: no cover
|
||||
from importlib_metadata import (
|
||||
PackageNotFoundError,
|
||||
version,
|
||||
) # pragma: no cover
|
||||
|
||||
try:
|
||||
# Change here if project is renamed and does not equal the package name
|
||||
|
|
|
@ -40,19 +40,27 @@ from NanoVNASaver.Touchstone import Touchstone
|
|||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description=__doc__,
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||
parser.add_argument("-d", "--debug", action="store_true",
|
||||
help="Set loglevel to debug")
|
||||
parser.add_argument("-D", "--debug-file",
|
||||
help="File to write debug logging output to")
|
||||
parser.add_argument("-f", "--file",
|
||||
help="Touchstone file to load as sweep for off"
|
||||
" device usage")
|
||||
parser.add_argument("-r", "--ref-file",
|
||||
help="Touchstone file to load as reference for off"
|
||||
" device usage")
|
||||
parser.add_argument("--version", action="version",
|
||||
version=f"NanoVNASaver {VERSION}")
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d", "--debug", action="store_true", help="Set loglevel to debug"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-D", "--debug-file", help="File to write debug logging output to"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--file",
|
||||
help="Touchstone file to load as sweep for off" " device usage",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--ref-file",
|
||||
help="Touchstone file to load as reference for off" " device usage",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version", action="version", version=f"NanoVNASaver {VERSION}"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
console_log_level = logging.WARNING
|
||||
|
@ -69,7 +77,8 @@ def main():
|
|||
ch = logging.StreamHandler()
|
||||
ch.setLevel(console_log_level)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
||||
"%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
||||
)
|
||||
ch.setFormatter(formatter)
|
||||
logger.addHandler(ch)
|
||||
|
||||
|
@ -81,8 +90,7 @@ def main():
|
|||
|
||||
logger.info("Startup...")
|
||||
|
||||
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling,
|
||||
True)
|
||||
QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_EnableHighDpiScaling, True)
|
||||
app = QtWidgets.QApplication(sys.argv)
|
||||
window = NanoVNASaver()
|
||||
window.show()
|
||||
|
@ -104,5 +112,5 @@ def main():
|
|||
raise exc
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
Ładowanie…
Reference in New Issue