Source code for tam.sadisplay

#
#    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 = []
[docs] def configure_freq(self): """ """ self.sa.freq = self.centre_freq self.sa.fspan = self.freq_span
[docs] def configure_ampl(self): """ """ self.sa.ref_level = self.reflevel if self.auto_att: self.sa.atten_auto = True else: self.sa.atten_auto = False self.sa.atten = self.att self.sa.mixer_level = self.max_mixer_level # The instrument ref. level may have been changed # due to coupling of instrument settings. # Retrieve the instrument ref. level and atten. settings... self.reflevel = self.sa.ref_level self.att = self.sa.atten
[docs] def configure_trace(self): """ """ self.sa.vavg = self.vavg self.sa.detector_mode = [*self.sa.DETECTOR_MODES][self.detector_mode]
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))