#
# Copyright (C) 2021 Dyadic Pty Ltd
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from typing import (
List, Dict, Optional
)
import sys
from functools import partial
from argparse import ArgumentParser
import asyncio
import numpy as np
from scipy import interpolate, signal
from qasync import (
QEventLoop, QThreadExecutor, asyncSlot
)
from tam import (
InstrumentMgr, InstrumentInitializeException,
UnknownInstrumentModelException,
HP8560A_GPIBID, DSA815_ID, DSA815, HP8560A
)
from PyQt5 import (
QtCore
)
from PyQt5.QtCore import (
Qt, QObject, QAbstractTableModel, pyqtSignal, pyqtSlot
)
from PyQt5.QtGui import (
QFont
)
from PyQt5.QtWidgets import (
QWidget, QLabel, QDoubleSpinBox, QLayout, QVBoxLayout, QHBoxLayout,
QGroupBox, QMainWindow, QComboBox, QPushButton, QMessageBox,
QFormLayout, QErrorMessage, QApplication, QTableView,
QStatusBar, QMenu, QGridLayout, QCheckBox, QFrame, QSpinBox,
QRadioButton, QGraphicsView, QToolBox, QSizePolicy, QButtonGroup,
QTabWidget
)
import pyqtgraph as pg
class ToiDataModel(QAbstractTableModel):
COLUMN_HEADER_TEXT = ['Freq. (MHz)', 'Power (dBm)']
ROW_HEADER_TEXT = ['Lower Tone', 'Upper Tone', 'Lower IM3', 'Upper IM3',
'IP3(1)', 'IP3(2)']
def __init__(self):
super().__init__()
self.initialize()
def __repr__(self):
return str(self._toi_table)
def __str__(self):
return f"""
lower tone: {self.lower_tone},
upper tone: {self.upper_tone},
lower IM3: {self.lower_im3},
upper IM3: {self.upper_im3},
ip3[0]: {self.ip3[0]},
ip3[1]: {self.ip3[1]}
"""
def initialize(self):
self._toi_table = [[0.0, 0.0] for i in range(6)]
@property
def lower_tone(self) -> List[float]:
return self._toi_table[0]
@lower_tone.setter
def lower_tone(self, t: List[float]) -> None:
self._toi_table[0] = t
@property
def upper_tone(self) -> List[float]:
return self._toi_table[1]
@upper_tone.setter
def upper_tone(self, t: List[float]) -> None:
self._toi_table[1] = t
@property
def lower_im3(self) -> List[float]:
return self._toi_table[2]
@lower_im3.setter
def lower_im3(self, t: List[float]) -> None:
self._toi_table[2] = t
@property
def upper_im3(self) -> List[float]:
return self._toi_table[3]
@upper_im3.setter
def upper_im3(self, t: List[float]) -> None:
self._toi_table[3] = t
def calculate_ip3(self) -> None:
self._toi_table[4][0] = (self.lower_tone[1] +
((self.upper_tone[1] - self.lower_im3[1])/2))
self._toi_table[5][0] = (self.upper_tone[1] +
((self.lower_tone[1] - self.upper_im3[1])/2))
@property
def ip3(self) -> List[float]:
return [self._toi_table[4][0], self._toi_table[5][0]]
def data(self, index, role):
if role == Qt.DisplayRole:
# row() indexes outer list
# col() indexes sub-list
row = index.row()
col = index.column()
if row in [4,5]:
if col == 0:
return ''
else:
return '{:4.1f}'.format(self.ip3[row-4])
if col == 0:
return '{:6.1f}'.format(self._toi_table[row][col])
else:
return '{:4.1f}'.format(self._toi_table[row][col])
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal:
if role == Qt.DisplayRole:
return ToiDataModel.COLUMN_HEADER_TEXT[section]
if orientation == Qt.Vertical:
if role == Qt.DisplayRole:
return ToiDataModel.ROW_HEADER_TEXT[section]
def rowCount(self, index):
return len(self._toi_table)
def columnCount(self, index):
return 2
[docs]class SADisplay(QObject):
FREQ_SUFFIXES = [' MHz', ' kHz', ' Hz']
PWR_SUFFIX = ' dB'
REFLEVEL_SUFFIX = ' dBm'
MIN_PLOT_WIDTH = 600
MIN_PLOT_HEIGHT = 400
PLOT_COLORS = [(52,138,189), (183,67,49), (142,186,66), (251,193,94),
(152,142,213), (255,181,184), (119,119,119)]
MKR_COLORS = [(255,153,153), (255,204,153), (255,255,153), (204,255,153),
(153,255,153), (153,255,204), (153,255,255), (153,204,255),
(153,153,255), (204,153,255), (255,153,255), (255,153,204),
(224,224,224)]
DET_POS = 0
DET_NORMAL = 1
DET_NEG = 2
DET_SAMPLE = 3
SWEEP_START = 0
SWEEP_COMPLETE = 1
TOI_MEASURE_START = 0
TOI_MEASURE_LOWER_IM3 = 1
TOI_MEASURE_UPPER_IM3 = 2
TOI_MEASURE_COMPLETE = 3
is_acquiring = pyqtSignal(bool)
sweep_state_changed = pyqtSignal(int)
sweep_updated = pyqtSignal()
toi_state_changed = pyqtSignal(int)
def __init__(self,
min_plot_height: int = MIN_PLOT_HEIGHT,
min_plot_width: int = MIN_PLOT_WIDTH) -> None:
"""
"""
super().__init__()
self._min_height = min_plot_height
self._min_width = min_plot_width
self._sa = None
self._min_freq: float = DSA815.MIN_FREQUENCY
self._max_freq: float = DSA815.MAX_FREQUENCY
self._min_reflevel: float = DSA815.MIN_REF_LEVEL
self._max_reflevel: float = DSA815.MAX_REF_LEVEL
self._reflevel_step: float = DSA815.REF_LEVEL_STEP
self._min_att: float = DSA815.MIN_ATT
self._max_att: float = DSA815.MAX_ATT
self._att_step: float = DSA815.ATT_STEP
self._min_mixlevel: float = DSA815.MIN_MAXMIXER_LEVEL
self._max_mixlevel: float = DSA815.MAX_MAXMIXER_LEVEL
self._mixlevel_step: float = DSA815.MAXMIXER_LEVEL_STEP
self._min_span: float = DSA815.MIN_SPAN
self._min_vavg: int = 0
self._max_vavg: int = 999
self.initialize_frequency_controls()
self.initialize_amplitude_controls()
self.initialize_trace_controls()
self.initialize_toi_controls()
self._det_mode_btns = []
self._spectrum_plot = None
self._spectrum_plot_data_item = None
self._sweep_data = {}
self._peak_markers = []
self._mkr_table = []
self.mkr_count = 1
self.peak_threshold = 10.0
self.toi_data = ToiDataModel()
@property
def sa(self):
return self._sa
@sa.setter
def sa(self, instr):
self._sa = instr
self._min_freq = instr.MIN_FREQUENCY
self._max_freq = instr.MAX_FREQUENCY
self._centre_freq = (self._max_freq - self._min_freq) / 2
self.set_freq_ctl_ranges()
self.min_reflevel = instr.MIN_REF_LEVEL
self.max_reflevel = instr.MAX_REF_LEVEL
self.reflevel_step = instr.REF_LEVEL_STEP
self.min_att = instr.MIN_ATT
self.max_att = instr.MAX_ATT
self.att_step = instr.ATT_STEP
self.min_mixlevel = instr.MIN_MAXMIXER_LEVEL
self.max_mixlevel = instr.MAX_MAXMIXER_LEVEL
self.mixlevel_step = instr.MAXMIXER_LEVEL_STEP
self._min_span = instr.MIN_SPAN
@property
def min_freq(self) -> float:
return self._min_freq
@min_freq.setter
def min_freq(self, f: float) -> None:
if self._min_freq != f:
self._min_freq = f
self.set_freq_ctl_ranges()
@property
def max_freq(self) -> float:
return self._max_freq
@max_freq.setter
def max_freq(self, f: float) -> None:
if self._max_freq != f:
self._max_freq = f
self.set_freq_ctl_ranges()
@property
def min_reflevel(self) -> float:
return self._min_reflevel
@min_reflevel.setter
def min_reflevel(self, p: float) -> None:
if self._min_reflevel != p:
self._min_reflevel = p
if self._reflevel_box is not None:
self._reflevel_box.setMinimum(self.min_reflevel)
@property
def max_reflevel(self) -> float:
return self._max_reflevel
@max_reflevel.setter
def max_reflevel(self, p: float) -> None:
if self._max_reflevel != p:
self._max_reflevel = p
if self._reflevel_box is not None:
self._reflevel_box.setMaximum(self.max_reflevel)
@property
def reflevel_step(self) -> float:
return self._reflevel_step
@reflevel_step.setter
def reflevel_step(self, r: float) -> None:
if self._reflevel_step != r:
self._reflevel_step = r
if self._reflevel_box is not None:
self._reflevel_box.setSingleStep(self.reflevel_step)
@property
def min_att(self) -> float:
return self._min_att
@min_att.setter
def min_att(self, p: float) -> None:
if self._min_att != p:
self._min_att = p
if self._att_box is not None:
self._att_box.setMinimum(self.min_att)
@property
def max_att(self) -> float:
return self._max_att
@max_att.setter
def max_att(self, p: float) -> None:
if self._max_att != p:
self._max_att = p
if self._att_box is not None:
self._att_box.setMaximum(self.max_att)
@property
def att_step(self) -> float:
return self._att_step
@att_step.setter
def att_step(self, a: float) -> None:
if self._att_step != a:
self._att_step = a
if self._att_box is not None:
self._att_box.setSingleStep(self.att_step)
@property
def min_mixlevel(self) -> float:
return self._min_mixlevel
@min_mixlevel.setter
def min_mixlevel(self, p: float) -> None:
if self._min_mixlevel != p:
self._min_mixlevel = p
if self._maxmix_box is not None:
self._maxmix_box.setMinimum(self.min_mixlevel)
if self._im3mixlevel_box is not None:
self._im3mixlevel_box.setMinimum(self.min_mixlevel)
@property
def max_mixlevel(self) -> float:
return self._max_mixlevel
@max_mixlevel.setter
def max_mixlevel(self, p: float) -> None:
if self._max_mixlevel != p:
self._max_mixlevel = p
if self._maxmix_box is not None:
self._maxmix_box.setMaximum(self.max_mixlevel)
if self._im3mixlevel_box is not None:
self._im3mixlevel_box.setMaximum(self.max_mixlevel)
@property
def mixlevel_step(self) -> float:
return self._mixlevel_step
@mixlevel_step.setter
def mixlevel_step(self, m: float) -> None:
if self._mixlevel_step != m:
self._mixlevel_step = m
if self._maxmix_box is not None:
self._maxmix_box.setSingleStep(self.mixlevel_step)
def set_freq_ctl_ranges(self) -> None:
if self._fcentre_box is not None:
self._fcentre_box.setRange(self.min_freq, self.max_freq)
if self._fstart_box is not None:
self._fstart_box.setRange(self.min_freq, self.max_freq)
if self._fstop_box is not None:
self._fstop_box.setRange(self.min_freq, self.max_freq)
def enable_freq_controls(self, b: bool) -> None:
self._freq_ctl_frame.setEnabled(b)
def enable_ampl_controls(self, b: bool) -> None:
self._ampl_ctl_frame.setEnabled(b)
def enable_trace_controls(self, b: bool) -> None:
self._trace_ctl_frame.setEnabled(b)
def enable_toi_controls(self, b: bool) -> None:
self._toi_ctl_frame.setEnabled(b)
def enable_sweep_button(self, b:bool) -> None:
self._sweep_btn.setEnabled(b)
def enable_controls(self, b: bool) -> None:
self._freq_ctl_frame.setEnabled(b)
self._ampl_ctl_frame.setEnabled(b)
self._trace_ctl_frame.setEnabled(b)
self._toi_ctl_frame.setEnabled(b)
self._sweep_btn.setEnabled(b)
def create_display_layout(self) -> QHBoxLayout:
self._display_layout = QHBoxLayout()
self._display_layout.setSizeConstraint(QLayout.SetMinimumSize)
vbox = QVBoxLayout()
pg.setConfigOptions(foreground='k')
self._graphic_layout = pg.GraphicsLayoutWidget()
self._graphic_layout.setMinimumWidth(self._min_width)
self._graphic_layout.setMinimumHeight(self._min_height)
self._graphic_layout.setSizePolicy(QSizePolicy.MinimumExpanding,
QSizePolicy.Minimum)
self._graphic_layout.setBackground('w')
self._graphic_layout.setViewportUpdateMode(
QGraphicsView.FullViewportUpdate)
self._spectrum_plot = self._graphic_layout.addPlot()
self._spectrum_plot.setMouseEnabled(x=False, y=False)
self._spectrum_plot.showAxis('right', True)
self._spectrum_plot.getAxis('right').setStyle(showValues=False)
self._spectrum_plot.showAxis('top', True)
self._spectrum_plot.getAxis('top').setStyle(showValues=False)
self._spectrum_plot.showGrid(True, True, alpha=0.15)
x_axis = self._spectrum_plot.getAxis('bottom')
x_axis.setStyle(showValues=False)
self.ref_text = pg.TextItem()
self.att_text = pg.TextItem()
self.startf_text = pg.TextItem()
self.fspan_text = pg.TextItem()
self.stopf_text = pg.TextItem()
self._spectrum_plot.addItem(self.ref_text)
self._spectrum_plot.addItem(self.att_text)
self._spectrum_plot.addItem(self.startf_text)
self._spectrum_plot.addItem(self.fspan_text)
self._spectrum_plot.addItem(self.stopf_text)
# self._spectrum_plot.sigYRangeChanged.connect(self._update_plot_annotations)
self._update_plot_annotations()
vbox.addWidget(self._graphic_layout)
vbox.addLayout(self._create_btn_layout())
self._display_layout.addLayout(vbox)
vbox = QVBoxLayout()
self._tb = QTabWidget()
self._tb.setSizePolicy(QSizePolicy.MinimumExpanding,
QSizePolicy.Preferred)
self._tb.addTab(self._create_freqctl_frame(), 'Frequency')
self._tb.addTab(self._create_amplctl_frame(), 'Amplitude')
self._tb.addTab(self._create_trace_frame(), 'Trace/Det.')
self._tb.addTab(self._create_toi_frame(), 'TOI')
vbox.addWidget(self._tb)
self._display_layout.addLayout(vbox)
self.sweep_updated.connect(self.plot_sweep_data)
return self._display_layout
def _create_btn_layout(self):
"""
"""
self._btn_layout = QHBoxLayout()
self._sweep_btn = QPushButton("Sweep")
self._sweep_btn.clicked.connect(self.sweep)
self._btn_layout.addStretch()
self._btn_layout.addWidget(self._sweep_btn)
return self._btn_layout
def _create_freqctl_frame(self) -> QFrame:
"""
"""
self._freq_ctl_frame = QFrame()
freq_vbox = QVBoxLayout()
self._freq_ctl_frame.setLayout(freq_vbox)
fbox = QFormLayout()
freq_vbox.addLayout(fbox)
self._fcentre_box = QDoubleSpinBox()
self._fcentre_box.setRange(self.min_freq, self.max_freq)
self._fcentre_box.setDecimals(3)
self._fcentre_box.setValue(self.centre_freq)
self._fcentre_box.setSuffix(SADisplay.FREQ_SUFFIXES[0])
self._fcentre_box.lineEdit().editingFinished.connect(
self.set_centre_freq)
fbox.addRow(QLabel("Centre Freq:"), self._fcentre_box)
self._fstart_box = QDoubleSpinBox()
self._fstart_box.setRange(self.min_freq, self.max_freq)
self._fstart_box.setDecimals(3)
self._fstart_box.setValue(self.start_freq)
self._fstart_box.setSuffix(SADisplay.FREQ_SUFFIXES[0])
self._fstart_box.lineEdit().editingFinished.connect(
self.set_start_freq)
fbox.addRow(QLabel("Start Freq:"), self._fstart_box)
self._fstop_box = QDoubleSpinBox()
self._fstop_box.setRange(self.min_freq, self.max_freq)
self._fstop_box.setDecimals(3)
self._fstop_box.setValue(self.stop_freq)
self._fstop_box.setSuffix(SADisplay.FREQ_SUFFIXES[0])
self._fstop_box.lineEdit().editingFinished.connect(
self.set_stop_freq)
fbox.addRow(QLabel("Stop Freq:"), self._fstop_box)
self._fspan_box = QDoubleSpinBox()
self._fspan_box.setRange(self._min_span, self.max_freq)
self._fspan_box.setDecimals(3)
self._fspan_box.setValue(self.freq_span)
self._fspan_box.setSuffix(SADisplay.FREQ_SUFFIXES[0])
self._fspan_box.lineEdit().editingFinished.connect(
self.set_freq_span)
fbox.addRow(QLabel("Span:"), self._fspan_box)
hbox = QHBoxLayout()
self._freq_config_btn = QPushButton("Configure")
self._freq_config_btn.clicked.connect(self.configure_freq)
hbox.addStretch()
hbox.addWidget(self._freq_config_btn)
freq_vbox.addLayout(hbox)
freq_vbox.addStretch()
return self._freq_ctl_frame
def initialize_frequency_controls(self):
self._start_freq: float = self.min_freq
self._stop_freq: float = self.max_freq
self._centre_freq: float = (
(self.max_freq - self.min_freq) / 2) + self.min_freq
self._freq_span: float = self.max_freq - self.min_freq
@property
def start_freq(self) -> float:
return self._start_freq
@start_freq.setter
def start_freq(self, f: float) -> None:
if self._start_freq != f:
self._update_freq_values(start=f)
def set_start_freq(self) -> None:
val = self._fstart_box.lineEdit().text()
self.start_freq = self._parse_freq_spec(val)
@property
def stop_freq(self) -> float:
return self._stop_freq
@stop_freq.setter
def stop_freq(self, f: float) -> None:
if self._stop_freq != f:
self._update_freq_values(stop=f)
def set_stop_freq(self) -> None:
val = self._fstop_box.lineEdit().text()
self.stop_freq = self._parse_freq_spec(val)
@property
def centre_freq(self) -> float:
return self._centre_freq
@centre_freq.setter
def centre_freq(self, f: float) -> None:
if self._centre_freq != f:
self._update_freq_values(centre=f)
def set_centre_freq(self) -> None:
val = self._fcentre_box.lineEdit().text()
self.centre_freq = self._parse_freq_spec(val)
@property
def freq_span(self) -> float:
return self._freq_span
@freq_span.setter
def freq_span(self, f: float) -> None:
if self._freq_span != f:
self._update_freq_values(span=f)
def set_freq_span(self) -> None:
val = self._fspan_box.lineEdit().text()
self.freq_span = self._parse_freq_spec(val)
def _create_amplctl_frame(self) -> QFrame:
"""
"""
self._ampl_ctl_frame = QFrame()
ampl_vbox = QVBoxLayout()
self._ampl_ctl_frame.setLayout(ampl_vbox)
fbox = QFormLayout()
self._reflevel_box = QDoubleSpinBox()
self._reflevel_box.setRange(self.min_reflevel, self.max_reflevel)
self._reflevel_box.setDecimals(1)
self._reflevel_box.setValue(self.reflevel)
self._reflevel_box.setSingleStep(self.reflevel_step)
self._reflevel_box.setSuffix(SADisplay.REFLEVEL_SUFFIX)
self._reflevel_box.lineEdit().editingFinished.connect(
self.set_reflevel)
fbox.addRow(QLabel("Ref. Level:"), self._reflevel_box)
self._att_box = QDoubleSpinBox()
self._att_box.setRange(self.min_att, self.max_att)
self._att_box.setDecimals(1)
self._att_box.setValue(self.att)
self._att_box.setSingleStep(self.att_step)
self._att_box.setSuffix(SADisplay.PWR_SUFFIX)
self._att_box.lineEdit().editingFinished.connect(
self.set_att)
self._auto_att_box = QCheckBox("Auto")
self._auto_att_box.setChecked(self.auto_att)
self._auto_att_box.stateChanged.connect(self.set_auto_att)
hbox = QHBoxLayout()
hbox.addWidget(self._att_box)
hbox.addWidget(self._auto_att_box)
fbox.addRow(QLabel("Input Att:"), hbox)
ampl_vbox.addLayout(fbox)
self._maxmix_box = QDoubleSpinBox()
self._maxmix_box.setRange(self.min_mixlevel, self.max_mixlevel)
self._maxmix_box.setDecimals(1)
self._maxmix_box.setValue(self.max_mixer_level)
self._maxmix_box.setSingleStep(self.mixlevel_step)
self._maxmix_box.setSuffix(SADisplay.REFLEVEL_SUFFIX)
self._maxmix_box.valueChanged.connect(self.set_max_mixer_level)
fbox.addRow(QLabel("Max. Mixer Level:"), self._maxmix_box)
hbox = QHBoxLayout()
self._ampl_config_btn = QPushButton("Configure")
self._ampl_config_btn.clicked.connect(self.configure_ampl)
hbox.addStretch()
hbox.addWidget(self._ampl_config_btn)
ampl_vbox.addLayout(hbox)
ampl_vbox.addStretch()
return self._ampl_ctl_frame
def initialize_amplitude_controls(self):
self._reflevel: float = 0.0
self._att: float = 10.0
self._auto_att: bool = False
self._max_mixer_level: float = -10.0
self._ampl_scale: float = 10.0
@property
def reflevel(self) -> float:
return self._reflevel
@reflevel.setter
def reflevel(self, p: float) -> None:
if self._reflevel != p:
self._reflevel = p
if self._reflevel_box is not None:
self._update_ampl_values(reflevel=p)
def set_reflevel(self) -> None:
val = self._reflevel_box.lineEdit().text()
self.reflevel = float(val.split()[0])
@property
def ampl_scale(self) -> float:
return self._ampl_scale
@ampl_scale.setter
def ampl_scale(self, s: float) -> None:
if self._ampl_scale != s:
self._ampl_scale = s
if self._spectrum_plot:
self._update_ampl_values()
@property
def att(self) -> float:
return self._att
@att.setter
def att(self, p: float) -> None:
if self._att != p:
self._att = p
if self._att_box is not None:
self._update_ampl_values(att=p)
def set_att(self) -> None:
val = self._att_box.lineEdit().text()
self.att = float(val.split()[0])
@property
def auto_att(self) -> bool:
return self._auto_att
@auto_att.setter
def auto_att(self, b: bool) -> None:
if self._auto_att != b:
self._auto_att = b
if self._auto_att_box is not None:
self._auto_att_box.setChecked(self.auto_att)
def set_auto_att(self, b) -> None:
self.auto_att = b
@property
def max_mixer_level(self) -> float:
return self._max_mixer_level
@max_mixer_level.setter
def max_mixer_level(self, p: float) -> None:
if self._max_mixer_level != p:
self._max_mixer_level = p
if self._maxmix_box is not None:
self._update_ampl_values(max_mixlevel=p)
def set_max_mixer_level(self, p: float) -> None:
self.max_mixer_level = p
def _create_trace_frame(self) -> QFrame:
"""
"""
self._trace_ctl_frame = QFrame()
trace_vbox = QVBoxLayout()
self._trace_ctl_frame.setLayout(trace_vbox)
fbox = QFormLayout()
trace_vbox.addLayout(fbox)
self._avgcount_box = QSpinBox()
self._avgcount_box.setRange(self._min_vavg, self._max_vavg)
self._avgcount_box.setValue(self.vavg)
self._avgcount_box.lineEdit().editingFinished.connect(
self.set_vavg)
fbox.addRow(QLabel("Averaging:"), self._avgcount_box)
vbox = QVBoxLayout()
self._det_btn_group = QButtonGroup()
hbox = QHBoxLayout()
self._det_mode_btns = []
rb = QRadioButton('Pos. Peak')
rb.toggled.connect(
partial(self.set_detector_mode, mode=SADisplay.DET_POS))
self._det_btn_group.addButton(rb)
hbox.addWidget(rb)
self._det_mode_btns.append(rb)
rb = QRadioButton('Normal')
rb.toggled.connect(
partial(self.set_detector_mode, mode=SADisplay.DET_NORMAL))
self._det_btn_group.addButton(rb)
hbox.addWidget(rb)
self._det_mode_btns.append(rb)
vbox.addLayout(hbox)
hbox = QHBoxLayout()
rb = QRadioButton('Neg. Peak')
self._det_btn_group.addButton(rb)
rb.toggled.connect(
partial(self.set_detector_mode, mode=SADisplay.DET_NEG))
hbox.addWidget(rb)
self._det_mode_btns.append(rb)
rb = QRadioButton('Sample')
rb.toggled.connect(
partial(self.set_detector_mode, mode=SADisplay.DET_SAMPLE))
self._det_btn_group.addButton(rb)
hbox.addWidget(rb)
self._det_mode_btns.append(rb)
self._det_mode_btns[self.detector_mode].setChecked(True)
vbox.addLayout(hbox)
fbox.addRow(QLabel('Detector:'), vbox)
hbox = QHBoxLayout()
self._trace_config_btn = QPushButton("Configure")
self._trace_config_btn.clicked.connect(self.configure_trace)
hbox.addStretch()
hbox.addWidget(self._trace_config_btn)
trace_vbox.addLayout(hbox)
trace_vbox.addStretch()
return self._trace_ctl_frame
def initialize_trace_controls(self):
self._vavg = 0
self._det_mode = SADisplay.DET_POS
@property
def vavg(self) -> int:
return self._vavg
@vavg.setter
def vavg(self, avg: int) -> None:
if self._vavg != avg:
self._vavg = avg
def set_vavg(self):
val = self._avgcount_box.lineEdit().text()
self.vavg = int(val)
@property
def detector_mode(self):
return self._det_mode
@detector_mode.setter
def detector_mode(self, det):
if self._det_mode != det:
self._det_mode = det
def set_detector_mode(self, state, mode):
if state:
self.detector_mode = mode
if len(self._det_mode_btns):
self._det_mode_btns[mode].setChecked(True)
def _create_toi_frame(self) -> QFrame:
"""
"""
self._toi_ctl_frame = QFrame()
toi_vbox = QVBoxLayout()
self._toi_ctl_frame.setLayout(toi_vbox)
fbox = QFormLayout()
toi_vbox.addLayout(fbox)
self._toitone_box = QDoubleSpinBox()
self._toitone_box.setRange(self.min_freq, self.max_freq)
self._toitone_box.setDecimals(3)
self._toitone_box.setValue(self.toi_tone_separation)
self._toitone_box.setSuffix(SADisplay.FREQ_SUFFIXES[0])
self._toitone_box.lineEdit().editingFinished.connect(
self.set_toi_tone_separation)
fbox.addRow(QLabel("Tone Separation:"), self._toitone_box)
self._toireflevel_box = QDoubleSpinBox()
self._toireflevel_box.setRange(self.min_reflevel, self.max_reflevel)
self._toireflevel_box.setDecimals(1)
self._toireflevel_box.setValue(self.toi_reflevel)
self._toireflevel_box.setSuffix(SADisplay.PWR_SUFFIX)
self._toireflevel_box.lineEdit().editingFinished.connect(
self.set_toi_reflevel)
fbox.addRow(QLabel("TOI Ref. level:"), self._toireflevel_box)
self._im3span_box = QDoubleSpinBox()
self._im3span_box.setRange(self.min_freq, self.max_freq)
self._im3span_box.setDecimals(3)
self._im3span_box.setValue(self.im3_span)
self._im3span_box.setSuffix(SADisplay.FREQ_SUFFIXES[0])
self._im3span_box.lineEdit().editingFinished.connect(
self.set_im3_span)
fbox.addRow(QLabel("IM3 Span:"), self._im3span_box)
self._im3reflevel_box = QDoubleSpinBox()
self._im3reflevel_box.setRange(self.min_reflevel, self.max_reflevel)
self._im3reflevel_box.setDecimals(1)
self._im3reflevel_box.setValue(self.im3_reflevel)
self._im3reflevel_box.setSuffix(SADisplay.REFLEVEL_SUFFIX)
self._im3reflevel_box.lineEdit().editingFinished.connect(
self.set_im3_reflevel)
fbox.addRow(QLabel("IM3 Ref. level:"), self._im3reflevel_box)
self._im3mixlevel_box = QDoubleSpinBox()
self._im3mixlevel_box.setRange(self.min_mixlevel, self.max_mixlevel)
self._im3mixlevel_box.setDecimals(1)
self._im3mixlevel_box.setValue(self.im3_mixlevel)
self._im3mixlevel_box.setSingleStep(self.mixlevel_step)
self._im3mixlevel_box.setSuffix(SADisplay.REFLEVEL_SUFFIX)
self._im3mixlevel_box.lineEdit().editingFinished.connect(
self.set_im3_mixlevel)
fbox.addRow(QLabel("IM3 Mixer level:"), self._im3mixlevel_box)
hbox = QHBoxLayout()
self._toi_measure_btn = QPushButton("Measure")
self._toi_measure_btn.clicked.connect(self.measure_toi)
hbox.addStretch()
hbox.addWidget(self._toi_measure_btn)
toi_vbox.addLayout(hbox)
toi_vbox.addStretch()
self.toi_table_view = QTableView()
self.toi_table_view.setSizePolicy(QSizePolicy.MinimumExpanding,
QSizePolicy.MinimumExpanding)
self.toi_table_view.setModel(self.toi_data)
toi_vbox.addWidget(self.toi_table_view)
self.toi_state_changed.connect(self.toi_state_updated)
return self._toi_ctl_frame
def initialize_toi_controls(self):
self._toi_fcentre = self.centre_freq
self._toi_tone_separation = 2.0
self._toi_reflevel = 0.0
self._im3_span = 0.01
self._im3_reflevel = -30.0
self._im3_mixlevel = -30.0
@property
def toi_tone_separation(self) -> float:
return self._toi_tone_separation
@toi_tone_separation.setter
def toi_tone_separation(self, f: float) -> None:
if self._toi_tone_separation != f:
self._toi_tone_separation = f
self._update_displayed_freq(
self._toitone_box, self.toi_tone_separation)
def set_toi_tone_separation(self) -> None:
val = self._toitone_box.lineEdit().text()
self.toi_tone_separation = self._parse_freq_spec(val)
@property
def toi_reflevel(self) -> float:
return self._toi_reflevel
@toi_reflevel.setter
def toi_reflevel(self, p: float) -> None:
if self._toi_reflevel != p:
self._toi_reflevel = p
if self._toireflevel_box is not None:
self._toireflevel_box.setValue(p)
def set_toi_reflevel(self) -> None:
val = self._toireflevel_box.lineEdit().text()
self.toi_reflevel = float(val.split()[0])
@property
def im3_span(self) -> float:
return self._im3_span
@im3_span.setter
def im3_span(self, f: float) -> None:
if self._im3_span != f:
self._im3_span = f
self._update_displayed_freq(self._im3span_box, self.im3_span)
def set_im3_span(self) -> None:
val = self._im3span_box.lineEdit().text()
self.im3_span = self._parse_freq_spec(val)
@property
def im3_reflevel(self) -> float:
return self._im3_reflevel
@im3_reflevel.setter
def im3_reflevel(self, p: float) -> None:
if self._im3_reflevel != p:
self._im3_reflevel = p
if self._im3reflevel_box is not None:
self._im3reflevel_box.setValue(p)
def set_im3_reflevel(self) -> None:
val = self._im3reflevel_box.lineEdit().text()
self.im3_reflevel = float(val.split()[0])
@property
def im3_mixlevel(self) -> float:
return self._im3_mixlevel
@im3_mixlevel.setter
def im3_mixlevel(self, p: float) -> None:
if self._im3_mixlevel != p:
self._im3_mixlevel = p
if self._im3mixlevel_box is not None:
self._im3mixlevel_box.setValue(p)
def set_im3_mixlevel(self) -> None:
val = self._im3mixlevel_box.lineEdit().text()
self.im3_mixlevel = float(val.split()[0])
@asyncSlot()
async def measure_toi(self):
self.toi_data.initialize()
self.toi_state_changed.emit(SADisplay.TOI_MEASURE_START)
loop = asyncio.get_event_loop()
await loop.run_in_executor(
None, self.acquire_sweep_data)
# Sort the fundamental tone markers by ascending frequency
# value.
if self._mkr_table[0][0] < self._mkr_table[1][0]:
self.toi_data.lower_tone = self._mkr_table[0]
self.toi_data.upper_tone = self._mkr_table[1]
else:
self.toi_data.lower_tone = self._mkr_table[1]
self.toi_data.upper_tone = self._mkr_table[0]
self.toi_state_changed.emit(SADisplay.TOI_MEASURE_LOWER_IM3)
await loop.run_in_executor(
None, self.acquire_sweep_data)
if len(self._mkr_table):
self.toi_data.lower_im3 = self._mkr_table[0]
self.toi_state_changed.emit(SADisplay.TOI_MEASURE_UPPER_IM3)
await loop.run_in_executor(
None, self.acquire_sweep_data)
if len(self._mkr_table):
self.toi_data.upper_im3 = self._mkr_table[0]
self.toi_state_changed.emit(SADisplay.TOI_MEASURE_COMPLETE)
self.toi_data.calculate_ip3()
def toi_state_updated(self, toi_state: int) -> None:
if toi_state == SADisplay.TOI_MEASURE_START:
self._toi_fcentre = self.centre_freq
self.freq_span = self.toi_tone_separation * 2
self.reflevel = self.toi_reflevel
self.configure_freq()
self.configure_ampl()
elif toi_state == SADisplay.TOI_MEASURE_LOWER_IM3:
self.centre_freq = self._toi_fcentre - \
(self.toi_tone_separation * 1.5)
self.freq_span = self.im3_span
self.reflevel = self.im3_reflevel
self.max_mixer_level = self.im3_mixlevel
self.configure_freq()
self.configure_ampl()
elif toi_state == SADisplay.TOI_MEASURE_UPPER_IM3:
self.centre_freq = self._toi_fcentre + \
(self.toi_tone_separation * 1.5)
self.freq_span = self.im3_span
self.reflevel = self.im3_reflevel
self.max_mixer_level = self.im3_mixlevel
self.configure_freq()
self.configure_ampl()
def initialize_toi_table(self):
self._toi_table = [[0.0, 0.0] for i in range(6)]
[docs] def acquire_sweep_data(self):
"""
"""
self.is_acquiring.emit(True)
self.enable_controls(False)
self.sa.sweep()
if isinstance(self.sa, HP8560A):
raw_data = np.array(self.sa.trace_raw_data())
sdata = self.reflevel + (self.ampl_scale * (raw_data - 600)/60)
else:
sdata = np.array(self.sa.trace_data())
self._sweep_data['pwr'] = sdata
self._sweep_data['freq'] = np.linspace(
self.start_freq, self.stop_freq, len(sdata))
self.enable_controls(True)
self.sweep_updated.emit()
self.is_acquiring.emit(False)
def plot_sweep_data(self):
self.remove_markers()
if self._spectrum_plot_data_item is None:
self._spectrum_plot_data_item = self._spectrum_plot.plot(
self._sweep_data['freq'],
self._sweep_data['pwr'],
pen=pg.mkPen(SADisplay.PLOT_COLORS[0], width=2))
else:
self._spectrum_plot_data_item.setData(
x=self._sweep_data['freq'],
y=self._sweep_data['pwr'])
self.add_markers()
@asyncSlot()
async def sweep(self):
self.sweep_state_changed.emit(SADisplay.SWEEP_START)
loop = asyncio.get_event_loop()
await loop.run_in_executor(
None, self.acquire_sweep_data)
self.sweep_state_changed.emit(SADisplay.SWEEP_COMPLETE)
def add_markers(self):
if self.mkr_count == 0:
return
mkr_count = self.mkr_count
self._mkr_table = [[0.0, 0.0] for i in range(mkr_count)]
pwr_arr = self._sweep_data['pwr']
freq_arr = self._sweep_data['freq']
peaks, _ = signal.find_peaks(
pwr_arr, prominence=self.peak_threshold)
peak_count = len(peaks)
if mkr_count > peak_count:
mkr_count = peak_count
sorted_peaks = sorted(peaks, key=lambda idx: pwr_arr[idx], reverse=True)
for count, peak in enumerate(sorted_peaks[:mkr_count]):
freq = freq_arr[peak]
pwr = pwr_arr[peak]
if pwr > (self.reflevel - (self.ampl_scale * 0.2)):
mkr_pos = (freq, self.reflevel)
mkr_angle = 90
else:
mkr_pos = (freq, pwr)
mkr_angle = -90
if count == 0:
mkr = pg.ArrowItem(pos=mkr_pos,
headLen=10,
angle=mkr_angle,
pen=pg.mkPen(
SADisplay.MKR_COLORS[0], width=2),
brush=(255,255,255))
else:
mkr = pg.ArrowItem(pos=mkr_pos,
headLen=10,
angle=mkr_angle)
self._spectrum_plot.addItem(mkr)
self._peak_markers.append(mkr)
self._mkr_table[count] = [freq, pwr]
def remove_markers(self):
for mkr in self._peak_markers:
self._spectrum_plot.removeItem(mkr)
self._peak_markers = []
self._mkr_table = []
def _update_freq_values(self, start=None, stop=None,
centre=None, span=None):
"""Keeps the start, stop, centre and span values consistent.
In addition, this method updates the spectrum plot frequency
axis (X axis).
"""
if start is not None:
stop = self.stop_freq
if start < self.min_freq:
start = self.min_freq
elif start > self.max_freq - self._min_span:
start = self.max_freq - self._min_span
span = stop - start
if span < self._min_span:
span = self._min_span
stop = start + span
centre = start + (span/2)
elif stop is not None:
start = self.start_freq
if stop < self.min_freq + self._min_span:
stop = self.min_freq + self._min_span
elif stop > self.max_freq:
stop = self.max_freq
span = stop - start
if span < self._min_span:
span = self._min_span
start = stop - span
centre = start + (span/2)
elif centre is not None:
# Keep the span constant if possible
start = self.start_freq
stop = self.stop_freq
span = self.freq_span
if centre < self.min_freq + self._min_span/2:
centre = self.min_freq + self._min_span/2
elif centre > self.max_freq - (self._min_span/2):
centre = self.max_freq - (self._min_span/2)
stop = centre + (span/2)
start = centre - (span/2)
if stop > self.max_freq:
stop = self.max_freq
span = 2 * (stop - centre)
start = centre - (span/2)
if start < self.min_freq:
start = self.min_freq
span = 2 * (centre - start)
stop = centre + (span/2)
elif span is not None:
# Keep the centre freq. constant if possible
centre = self.centre_freq
if span < self._min_span:
span = self._min_span
elif span > self.max_freq - self.min_freq:
span = self.max_freq - self.min_freq
stop = centre + (span/2)
if stop > self.max_freq:
stop = self.max_freq
span = stop - start
centre = start + (span/2)
start = stop - span
if start < self.min_freq:
start = self.min_freq
span = stop - start
centre = start + (span/2)
else:
return
self._start_freq = start
self._stop_freq = stop
self._centre_freq = centre
self._freq_span = span
self._update_displayed_freq(self._fstart_box, self.start_freq)
self._update_displayed_freq(self._fstop_box, self.stop_freq)
self._update_displayed_freq(self._fcentre_box, self.centre_freq)
self._update_displayed_fspan(self._fspan_box, self.freq_span)
self._update_plot_annotations()
def _parse_freq_spec(self, s):
f_disp, f_suffix = s.split()
if f_suffix == SADisplay.FREQ_SUFFIXES[2].strip():
return float(f_disp) / 1e6
elif f_suffix == SADisplay.FREQ_SUFFIXES[1].strip():
return float(f_disp) / 1e3
else:
return float(f_disp)
def _update_displayed_fspan(self, w, f):
if f < 0.001:
f_disp = f * 1e6
f_suffix = SADisplay.FREQ_SUFFIXES[2]
w.setRange(self._min_span * 1e6, self.max_freq * 1e6)
elif f < 1.0:
f_disp = f * 1e3
f_suffix = SADisplay.FREQ_SUFFIXES[1]
w.setRange(self._min_span * 1e3, self.max_freq * 1e3)
else:
f_disp = f
f_suffix = SADisplay.FREQ_SUFFIXES[0]
w.setRange(self._min_span, self.max_freq)
w.setValue(f_disp)
w.setSuffix(f_suffix)
def _update_displayed_freq(self, w, f):
if f < 0.001:
f_disp = f * 1e6
f_suffix = SADisplay.FREQ_SUFFIXES[2]
w.setRange(self.min_freq * 1e6, self.max_freq * 1e6)
elif f < 1.0:
f_disp = f * 1e3
f_suffix = SADisplay.FREQ_SUFFIXES[1]
w.setRange(self.min_freq * 1e3, self.max_freq * 1e3)
else:
f_disp = f
f_suffix = SADisplay.FREQ_SUFFIXES[0]
w.setRange(self.min_freq, self.max_freq)
w.setValue(f_disp)
w.setSuffix(f_suffix)
def _update_ampl_values(self, reflevel=None, att=None,
max_mixlevel=None):
if reflevel is not None:
self._reflevel_box.setValue(reflevel)
if att is not None:
self._att_box.setValue(att)
if max_mixlevel is not None:
self._maxmix_box.setValue(max_mixlevel)
self._update_plot_annotations()
def _format_freq(self, f: float) -> str:
"""f is in MHz
"""
if f < 0.001:
return "{:4.1f}{}".format(
f * 1e6, SADisplay.FREQ_SUFFIXES[2])
elif f < 1.0:
return "{:6.2f}{}".format(
f * 1e3, SADisplay.FREQ_SUFFIXES[1])
else:
return "{:7.3f}{}".format(
f, SADisplay.FREQ_SUFFIXES[0])
def _update_plot_annotations(self):
rl_low = self.reflevel - 10*self.ampl_scale
self._spectrum_plot.setXRange(self.start_freq, self.stop_freq,
padding=0.0)
self._spectrum_plot.setYRange(rl_low, self.reflevel, padding=0.0)
xtick_posns = np.linspace(self.start_freq, self.stop_freq, 11)
xticks = []
for f in xtick_posns:
xticks.append((f, ''))
self._spectrum_plot.getAxis('bottom').setTicks([xticks])
self._spectrum_plot.getAxis('top').setTicks([xticks])
self.ref_text.setText(f"Ref: {self.reflevel} dBm")
self.ref_text.setPos(xtick_posns[0], self.reflevel)
self.att_text.setText(f"Att: {self.att} dB")
self.att_text.setPos(xtick_posns[4], self.reflevel)
self.startf_text.setText(f"Start: {self._format_freq(self.start_freq)}")
self.startf_text.setPos(xtick_posns[0], rl_low + (self.ampl_scale * 0.6))
self.fspan_text.setText(f"Span: {self._format_freq(self.freq_span)}")
self.fspan_text.setPos(xtick_posns[4], rl_low + (self.ampl_scale * 0.6))
self.stopf_text.setText(f"Stop: {self._format_freq(self.stop_freq)}")
self.stopf_text.setPos(xtick_posns[7], rl_low + (self.ampl_scale * 0.6))