Source code for pyfda.input_widgets.freq_units

# -*- coding: utf-8 -*-
#
# This file is part of the pyFDA project hosted at https://github.com/chipmuenk/pyfda
#
# Copyright © pyFDA Project Contributors
# Licensed under the terms of the MIT License
# (see file LICENSE in root directory for details)

"""
Subwidget for entering frequency units
"""
import sys
from pyfda.libs.compat import (
    QtCore, QWidget, QLabel, QLineEdit, QComboBox, QFrame, QFont, QSizePolicy,
    QIcon, QVBoxLayout, QHBoxLayout, QGridLayout, pyqtSignal, QEvent)

import pyfda.filterbroker as fb
from pyfda.libs.pyfda_lib import to_html, safe_eval, pprint_log, first_item
from pyfda.libs.pyfda_qt_lib import qget_cmb_box, qset_cmb_box, qcmb_box_populate, PushButton
from pyfda.pyfda_rc import params  # FMT string for QLineEdit fields, e.g. '{:.3g}'

import logging
logger = logging.getLogger(__name__)


[docs] class FreqUnits(QWidget): """ Build and update widget for entering frequency unit, frequency range and sampling frequency f_S The following key-value pairs of the `fb.fil[0]` dict are modified: - `'freq_specs_unit'` : The unit ('f_S', 'f_Ny', 'Hz' etc.) as a string - `'freqSpecsRange'` : A list with two entries for minimum and maximum frequency values for labelling the frequency axis - `'f_S'` : The sampling frequency for referring frequency values to as a float - `'f_max'` : maximum frequency for scaling frequency axis - `'plt_fUnit'`: frequency unit as string - `'plt_tUnit'`: time unit as string - `'plt_fLabel'`: label for frequency axis - `'plt_tLabel'`: label for time axis """ # class variables (shared between instances if more than one exists) # incoming: sig_rx = pyqtSignal(object) # outgoing: from various and when normalized frequencies have been changed sig_tx = pyqtSignal(object) # outgoing from pyfda.libs.pyfda_qt_lib import emit def __init__(self, parent=None, title="Frequency Units", objectName=""): super(FreqUnits, self).__init__(parent) self.title = title self.setObjectName(objectName) self.spec_edited = False # flag whether QLineEdit field has been edited # combobox tooltip + data / text / tooltip for frequency unit self.cmb_f_unit_items = [ "<span>Select whether frequencies are specified w.r.t. the sampling " "frequency " + to_html("f_S", frmt = 'i') + ", to the Nyquist frequency " + to_html("f_Ny = f_S", frmt='i') + "/2 or as absolute values.", ("fs", "f_S", "Relative to sampling frequency, " + to_html("F = f / f_S", frmt='i')), ("fny", "f_Ny", "Relative to Nyquist frequency, " + to_html("F = f / f_Ny = 2f / f_S", frmt='i')), # ("k", "k", "Frequency index " + to_html("k = 0 ... N_FFT - 1", frmt='i')), ("mhz", "mHz", "Absolute sampling frequency in mHz"), ("hz", "Hz", "Absolute sampling frequency in Hz"), ("khz", "kHz", "Absolute sampling frequency in kHz"), ("meghz", "MHz", "Absolute sampling frequency in MHz"), ("ghz", "GHz", "Absolute sampling frequency in GHz") ] self.cmb_f_unit_init = "fs" self.cmb_f_range_items = [ "Select one- or two-sided spectrum and symmetry around <i>f</i> = 0", ("half", "0...½", "One-sided spectrum"), ("whole", "0...1", "Two-sided spectrum, starting at <i>f</i> = 0"), ("sym", "-½...½", "Two-sided spectrum, symmetrical around <i>f</i> = 0") ] self.cmb_f_range_init = "half" # t_units and f_scale have the same index as the f_unit_items, i.e. # 'f_S', 'f_Ny', 'mHz', 'Hz', 'kHz', 'MHz', 'GHz' self.t_units = ['T_S', 'T_S', 'ks', 's', 'ms', r'$\mu$s', 'ns'] self.f_scale = [1, 1, 1e-3, 1, 1e3, 1e6, 1e9] self._construct_UI() # ------------------------------------------------------------------------------
[docs] def process_sig_rx(self, dict_sig=None): """ Process signals coming from - FFT window widget - qfft_win_select """ # logger.warning(f"SIG_RX: {first_item(dict_sig)}") if 'id' in dict_sig and dict_sig['id'] == id(self): logger.debug("Stopped infinite loop") return elif ('view_changed' in dict_sig and dict_sig['view_changed'] == 'f_S')\ or 'data_changed' in dict_sig: self.update_UI(emit=False)
# ------------------------------------------------------------------------------ def _construct_UI(self): """ Construct the User Interface """ self.layVMain = QVBoxLayout() # Widget main layout bfont = QFont() bfont.setBold(True) self.lblUnits = QLabel(self) self.lblUnits.setText("Freq. Unit") self.lblUnits.setFont(bfont) self.f_s_old = fb.fil[0]['f_S'] # store current sampling frequency self.T_s_old = fb.fil[0]['T_S'] # store current sampling period self.lbl_f_s = QLabel(self) self.lbl_f_s.setText(to_html("f_S =", frmt='bi')) self.led_f_s = QLineEdit(objectName="f_S") self.led_f_s.setText(str(fb.fil[0]["f_S"])) self.led_f_s.installEventFilter(self) # filter events self.butLock = PushButton(self, icon=QIcon(':/lock-unlocked.svg')) self.butLock.setToolTip( "<span><b>Unlocked:</b> When <i>f<sub>S</sub></i> is changed, all frequency " "related widgets are updated, normalized frequencies stay the same.<br />" "<b>Locked:</b> When <i>f<sub>S</sub></i> is changed, displayed absolute " "frequency values don't change but normalized frequencies do.</span>") layHF_S = QHBoxLayout() layHF_S.addWidget(self.led_f_s) layHF_S.addWidget(self.butLock) self.cmb_f_units = QComboBox(self, objectName="cmb_f_units") qcmb_box_populate(self.cmb_f_units, self.cmb_f_unit_items, self.cmb_f_unit_init) # self.cmb_f_units.setItemData(0, (0,QColor("#FF333D"),Qt.BackgroundColorRole))# # self.cmb_f_units.setItemData(0, (QFont('Verdana', bold=True), Qt.FontRole) self.cmb_f_range = QComboBox(self, objectName="cmb_f_range") qcmb_box_populate(self.cmb_f_range, self.cmb_f_range_items, self.cmb_f_range_init) # Combobox resizes with longest entry self.cmb_f_units.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.cmb_f_range.setSizeAdjustPolicy(QComboBox.AdjustToContents) self.butSort = PushButton(self, icon=QIcon(':/sort-ascending.svg')) self.butSort.setChecked(True) self.butSort.setToolTip("Sort frequencies in ascending order when activated.") self.butSort.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.layHUnits = QHBoxLayout() self.layHUnits.addWidget(self.cmb_f_units) self.layHUnits.addWidget(self.cmb_f_range) self.layHUnits.addWidget(self.butSort) # Create a gridLayout consisting of QLabel and QLineEdit fields # for setting f_S, the units and the actual frequency specs: self.layGSpecWdg = QGridLayout() # sublayout for spec fields self.layGSpecWdg.addWidget(self.lbl_f_s, 1, 0) # self.layGSpecWdg.addWidget(self.led_f_s,1,1) self.layGSpecWdg.addLayout(layHF_S, 1, 1) self.layGSpecWdg.addWidget(self.lblUnits, 0, 0) self.layGSpecWdg.addLayout(self.layHUnits, 0, 1) frmMain = QFrame(self) frmMain.setLayout(self.layGSpecWdg) self.layVMain.addWidget(frmMain) self.layVMain.setContentsMargins(*params['wdg_margins']) self.setLayout(self.layVMain) #---------------------------------------------------------------------- # GLOBAL SIGNALS & SLOTs #---------------------------------------------------------------------- self.sig_rx.connect(self.process_sig_rx) #---------------------------------------------------------------------- # LOCAL SIGNALS & SLOTs #---------------------------------------------------------------------- # swallow index passed by "IndexChanged": self.cmb_f_units.currentIndexChanged.connect(lambda: self.update_UI(self)) self.butLock.clicked.connect(self._lock_freqs) self.cmb_f_range.currentIndexChanged.connect(self._freq_range) self.butSort.clicked.connect(self._store_sort_flag) # ---------------------------------------------------------------------- self.update_UI() # first-time initialization # ------------------------------------------------------------- def _lock_freqs(self): """ Lock / unlock frequency entries: The values of frequency related widgets are stored in normalized form (w.r.t. sampling frequency)`fb.fil[0]['f_S']`. When the sampling frequency changes, absolute frequencies displayed in the widgets change their values. Most of the time, this is the desired behaviour, the properties of discrete time systems or signals are usually defined by the normalized frequencies. When the effect of varying the sampling frequency is to be analyzed, the displayed values in the widgets can be locked by pressing the Lock button. After changing the sampling frequency, normalized frequencies have to be rescaled like `f_a *= fb.fil[0]['f_S_prev'] / fb.fil[0]['f_S']` to maintain the displayed value `f_a * f_S`. This has to be accomplished by each frequency widget (currently, these are freq_specs and plot_tran_stim) when receiving the signal {'view_changed': 'f_S'}. The setting is stored as bool in the global dict entry `fb.fil[0]['freq_locked'`. No signal is emitted because there is no immediate need for action, all the values remain unchanged. """ if self.butLock.checked: # Lock has been activated, keep displayed frequencies locked fb.fil[0]['freq_locked'] = True self.butLock.setIcon(QIcon(':/lock-locked.svg')) else: # Lock has been unlocked, scale displayed frequencies with f_S fb.fil[0]['freq_locked'] = False self.butLock.setIcon(QIcon(':/lock-unlocked.svg')) # -------------------------------------------------------------
[docs] def update_UI(self, emit=True): """ update_UI is called - during init (direct call) - when the unit combobox is changed (signal-slot) - when a signal {'view_changed': 'f_S'} or {'data_changed': ...} has been received. In this case, the UI is updated from the fb.fil[0] dictionary and no signal is emitted (`emit==False`). Set various scale factors and labels depending on the setting of the unit combobox. Update the freqSpecsRange and finally, emit 'view_changed':'f_S' signal """ if not emit: # triggered by function call, not by a change of UI # Load f_S display from dict self.led_f_s.setText(str(fb.fil[0]['f_S'])) # Load freq. unit setting from dict idx = qset_cmb_box(self.cmb_f_units, fb.fil[0]['freq_specs_unit'], caseSensitive=True) if idx == -1: logger.warning( f"Unknown frequency unit {fb.fil[0]['freq_specs_unit']}, " "using 'f_S'.") # Load Frequency range type (0 ... f_S/2 etc.) from dict qset_cmb_box(self.cmb_f_range, fb.fil[0]['freqSpecsRangeType'], data=True, fireSignals=True) f_unit = qget_cmb_box(self.cmb_f_units, data=False) # selected frequency unit, idx = self.cmb_f_units.currentIndex() # its index f_s_scale = self.f_scale[idx] # and its scaling factor is_normalized_freq = f_unit in {"f_S", "f_Ny"} self.led_f_s.setVisible(not is_normalized_freq) # only vis. when self.lbl_f_s.setVisible(not is_normalized_freq) # not normalized self.butLock.setVisible(not is_normalized_freq) if is_normalized_freq: # store current sampling frequency to restore it when returning to # absolute (not normalized) frequencies if f_unit == "f_S": # normalized to f_S fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 1. f_label = r"$F = f\, /\, f_S = \Omega \, /\, 2 \mathrm{\pi} \; \rightarrow$" elif f_unit == "f_Ny": # normalized to f_nyq = f_S / 2 fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = 2. f_label = r"$F = 2f \, / \, f_S = \Omega \, / \, \mathrm{\pi} \; \rightarrow$" else: # frequency index k, logger.error("Unit k is no longer supported!") # always use T_S = 1 for normalized frequencies fb.fil[0]['T_S'] = 1. t_label = r"$n = t\, /\, T_S \; \rightarrow$" # Don't lock frequency scaling with normalized frequencies fb.fil[0]['freq_locked'] = False self.butLock.setIcon(QIcon(':/lock-unlocked.svg')) else: # Hz, kHz, ... # Restore sampling frequency when selecting absolute instead of # normalized frequencies if fb.fil[0]['freq_specs_unit'] in {"f_S", "f_Ny"}: # previous setting normalized? fb.fil[0]['f_S'] = fb.fil[0]['f_max'] = self.f_s_old # yes, restore prev. f_S fb.fil[0]['T_S'] = self.T_s_old # yes, restore prev. T_S # --- try to pick the most suitable unit for f_S -------------- f_S = fb.fil[0]['f_S'] * f_s_scale if f_S >= 1e9: f_unit = "GHz" elif f_S >= 1e6: f_unit = "MHz" elif f_S >= 1e3: f_unit = "kHz" elif f_S >= 1: f_unit = "Hz" else: f_unit = "mHz" new_idx = qset_cmb_box(self.cmb_f_units, f_unit, caseSensitive=True) if new_idx != idx: # sampling frequency unit has been changed, f_S and T_S need to be scaled idx = new_idx f_s_scale = self.f_scale[idx] fb.fil[0]['f_S'] = f_S / f_s_scale fb.fil[0]['T_S'] = f_s_scale / f_S emit = True # ------------------------------------------------------------- self.f_s_old = fb.fil[0]['f_S'] self.T_s_old = fb.fil[0]['T_S'] self.led_f_s.setText(params['FMT'].format(fb.fil[0]['f_S'])) f_label = r"$f$ in " + f_unit + r"$\; \rightarrow$" t_label = r"$t$ in " + self.t_units[idx] + r"$\; \rightarrow$" fb.fil[0].update({'f_s_scale': f_s_scale}) # scale factor for f_S (Hz, kHz, ...) fb.fil[0].update({'freq_specs_unit': f_unit}) # frequency unit # time and frequency unit as string e.g. for plot axis labeling fb.fil[0].update({"plt_fUnit": f_unit}) fb.fil[0].update({"plt_tUnit": self.t_units[idx]}) # complete plot axis labels including unit and arrow fb.fil[0].update({"plt_fLabel": f_label}) fb.fil[0].update({"plt_tLabel": t_label}) self._freq_range(emit=False) # update f_lim setting without emitting signal if emit: # UI was updated by user or a rescaling of f_S self.emit({'view_changed': 'f_S'})
# ------------------------------------------------------------------------------
[docs] def eventFilter(self, source, event): """ Filter all events generated by the QLineEdit `f_S` widget. Source and type of all events generated by monitored objects are passed to this eventFilter, evaluated and passed on to the next hierarchy level. - When a QLineEdit widget gains input focus (QEvent.FocusIn`), display the stored value from filter dict with full precision - When a key is pressed inside the text field, set the `spec_edited` flag to True. - When a QLineEdit widget loses input focus (QEvent.FocusOut`), store current value with full precision (only if `spec_edited`== True) and display the stored value in selected format. Emit 'view_changed':'f_S' - When f_S has been changed, update `fb.fil[0]['f_S']`, emit `{'view_changed': 'f_S'}` to update other widgets and only *then* update {'f_S_prev': fb.fil[0]['f_S']} to allow correction of normalized frequency with the old value of f_S. """ def _store_entry(): """ Update filter dictionary with sampling frequency and related parameters and emit `{'view_changed': 'f_S'}`. """ if self.spec_edited: f_S_tmp = safe_eval(source.text(), fb.fil[0]['f_S'], sign='pos') fb.fil[0].update({'f_S': f_S_tmp}) fb.fil[0].update({'T_S': 1./f_S_tmp}) fb.fil[0].update({'f_max': f_S_tmp}) self._freq_range(emit=False) # update plotting range self.emit({'view_changed': 'f_S'}) # Now store current f_S as f_S_prev fb.fil[0].update({'f_S_prev': fb.fil[0]['f_S']}) self.spec_edited = False # reset flag, changed entry has been saved # ---------------------- if source.objectName() == 'f_S': if event.type() == QEvent.FocusIn: self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.KeyPress: self.spec_edited = True # entry has been changed key = event.key() if key in {QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter}: _store_entry() elif key == QtCore.Qt.Key_Escape: # revert changes self.spec_edited = False source.setText(str(fb.fil[0]['f_S'])) # full precision elif event.type() == QEvent.FocusOut: _store_entry() source.setText(params['FMT'].format(fb.fil[0]['f_S'])) # reduced prec. # Call base class method to continue normal event processing: return super(FreqUnits, self).eventFilter(source, event)
# ------------------------------------------------------------- def _freq_range(self, emit=True): """ Set frequency plotting range for single-sided spectrum up to f_S/2 or f_S or for double-sided spectrum between -f_S/2 and f_S/2 Emit 'view_changed':'f_range' when `emit=True` """ if type(emit) == int: # signal was emitted by combobox emit = True rangeType = qget_cmb_box(self.cmb_f_range) fb.fil[0].update({'freqSpecsRangeType': rangeType}) f_max = fb.fil[0]["f_max"] if rangeType == 'whole': f_lim = [0, f_max] elif rangeType == 'sym': f_lim = [-f_max/2., f_max/2.] else: f_lim = [0, f_max/2.] fb.fil[0]['freqSpecsRange'] = f_lim # store settings in dict if emit: self.emit({'view_changed': 'f_range'}) # -------------------------------------------------------------
[docs] def load_dict(self): """ Reload comboBox settings and textfields from filter dictionary Block signals during update of combobox / lineedit widgets This is called from `input_specs.load_dict()` """ self.update_UI(emit=False)
# This updates the following widgets: # - `self.led_f_s` from `fb.fil[0]['f_S']` # - `self.cmb_f_units` with `fb.fil[0]['freq_specs_unit']` # - `self.cmb_f_range` from `fb.fil[0]['freqSpecsRangeType']`` # The other widgets are updated automatically. # ------------------------------------------------------------- def _store_sort_flag(self): """ Store sort flag in filter dict and emit 'specs_changed':'f_sort' when sort button is checked. """ fb.fil[0]['freq_specs_sort'] = self.butSort.checked if self.butSort.checked: self.emit({'specs_changed': 'f_sort'})
# ------------------------------------------------------------------------------ if __name__ == '__main__': """ Run widget standalone with `python -m pyfda.input_widgets.freq_units` """ from pyfda.libs.compat import QApplication from pyfda import pyfda_rc as rc app = QApplication(sys.argv) app.setStyleSheet(rc.qss_rc) mainw = FreqUnits() app.setActiveWindow(mainw) mainw.update_UI() # mainw.updateUI(newLabels = ['F_PB','F_PB2']) mainw.show() sys.exit(app.exec_())