Source code for pyfda.filter_widgets.ma

# -*- 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)

"""
Design Moving-Average-Filters (LP, HP) with fixed order, return
the filter design in coefficients format ('ba') or as poles/zeros ('zpk')

Attention:
This class is re-instantiated dynamically everytime the filter design method
is selected, calling the __init__ method.

API version info:
    1.0: initial working release
    1.1: mark private methods as private
    1.2: - new API using fil_save & fil_convert (allow multiple formats,
                save 'ba' _and_ 'zpk' precisely)
         - include method _store_entries in _update_UI
    1.3: new public methods destruct_UI + construct_UI (no longer called by __init__)
    1.4: module attribute `filter_classes` contains class name and combo box name
         instead of class attribute `name`
        `FRMT` is now a class attribute
    2.0: Specify the parameters for each subwidget as tuples in a dict where the
         first element controls whether the widget is visible and / or enabled.
         This dict is now called self.rt_dict. When present, the dict self.rt_dict_add
         is read and merged with the first one.
    2.1: Remove method destruct_UI and attributes self.wdg and self.hdl

   :2.2: Rename `filter_classes` -> `classes`, remove Py2 compatibility
"""
from pyfda.libs.compat import (QWidget, QLabel, QLineEdit, pyqtSignal, QCheckBox,
                      QVBoxLayout, QHBoxLayout)

import numpy as np

import pyfda.filterbroker as fb
from pyfda.libs.pyfda_lib import fil_save, fil_convert, ceil_odd, safe_eval
from pyfda.libs.pyfda_qt_lib import popup_warning
from pyfda.libs.pyfda_sig_lib import zeros_with_val

__version__ = "2.2"

classes = {'MA':'Moving Average'} #: Dict containing class name : display name

[docs] class MA(QWidget): FRMT = ('zpk', 'ba') # output format(s) of filter design routines 'zpk' / 'ba' / 'sos' info =""" **Moving average filters** can only be specified via their length and the number of cascaded sections. The minimum order to obtain a certain attenuation at a given frequency is calculated via the si function. Moving average filters can be implemented very efficiently in hard- and software as they require no multiplications but only addition and subtractions. Probably only the lowpass is really useful, as the other response types only filter out resp. leave components at ``f_S/4`` (bandstop resp. bandpass) resp. leave components near ``f_S/2`` (highpass). **Design routines:** ``ma.calc_ma()`` """ sig_tx = pyqtSignal(object) from pyfda.libs.pyfda_qt_lib import emit def __init__(self, objectName='ma_inst'): QWidget.__init__(self) self.setObjectName(objectName) self.delays = 12 # number of delays per stage self.stages = 1 # number of stages self.ft = 'FIR' self.rt_dicts = () # Common data for all filter response types: # This data is merged with the entries for individual response types # (common data comes first): self.rt_dict = { 'COM':{'man':{'fo': ('d', 'N'), 'msg':('a', "Enter desired order (= delays) <b><i>M</i></b> per stage and" " the number of <b>stages</b>. Target frequencies and amplitudes" " are only used for comparison, not for the design itself.") }, 'min':{'fo': ('d', 'N'), 'msg':('a', "Enter desired attenuation <b><i>A<sub>SB</sub></i></b> at " "the corner of the stop band <b><i>F<sub>SB</sub></i></b>. " "Choose the number of <b>stages</b>, the minimum order <b><i>M</i></b> " "per stage will be determined. Passband specs are not regarded.") } }, 'LP': {'man':{'tspecs': ('u', {'frq':('u','F_PB','F_SB'), 'amp':('u','A_PB','A_SB')}) }, 'min':{'tspecs': ('a', {'frq':('a','F_PB','F_SB'), 'amp':('a','A_PB','A_SB')}) } }, 'HP': {'man':{'tspecs': ('u', {'frq':('u','F_SB','F_PB'), 'amp':('u','A_SB','A_PB')}) }, 'min':{'tspecs': ('a', {'frq':('a','F_SB','F_PB'), 'amp':('a','A_SB','A_PB')}) }, }, 'BS': {'man':{'tspecs': ('u', {'frq':('u','F_PB','F_SB','F_SB2', 'F_PB2'), 'amp':('u','A_PB','A_SB','A_PB2')}), 'msg': ('a', "\nThis is not a proper band stop, it only lets pass" " frequency components around DC and <i>f<sub>S</sub></i>/2." " The order needs to be odd."), }}, 'BP': {'man':{'tspecs': ('u', {'frq':('u','F_SB','F_PB','F_PB2','F_SB2',), 'amp':('u','A_SB','A_PB','A_SB2')}), 'msg': ('a', "\nThis is not a proper band pass, it only lets pass" " frequency components around <i>f<sub>S</sub></i>/4." " The order needs to be odd."), }}, } self.info_doc = [] # self.info_doc.append('remez()\n=======') # self.info_doc.append(sig.remez.__doc__) # self.info_doc.append('remezord()\n==========') # self.info_doc.append(remezord.__doc__) self.construct_UI() #--------------------------------------------------------------------------
[docs] def construct_UI(self): """ Create additional subwidget(s) needed for filter design: These subwidgets are instantiated dynamically when needed in select_filter.py using the handle to the filter instance, fb.fil_inst. """ self.lbl_delays = QLabel("<b><i>M =</ i></ b>", self) self.lbl_delays.setObjectName('wdg_lbl_ma_0') self.led_delays = QLineEdit(self) try: self.led_delays.setText(str(fb.fil[0]['N'])) except KeyError: self.led_delays.setText(str(self.delays)) self.led_delays.setObjectName('wdg_led_ma_0') self.led_delays.setToolTip("Set number of delays per stage") self.lbl_stages = QLabel("<b>Stages =</ b>", self) self.lbl_stages.setObjectName('wdg_lbl_ma_1') self.led_stages = QLineEdit(self) self.led_stages.setText(str(self.stages)) self.led_stages.setObjectName('wdg_led_ma_1') self.led_stages.setToolTip("Set number of stages ") self.chk_norm = QCheckBox("Normalize", self) self.chk_norm.setChecked(True) self.chk_norm.setObjectName('wdg_chk_ma_2') self.chk_norm.setToolTip("Normalize to| H_max = 1|") self.layHWin = QHBoxLayout() self.layHWin.setObjectName('wdg_layGWin') self.layHWin.addWidget(self.lbl_delays) self.layHWin.addWidget(self.led_delays) self.layHWin.addStretch(1) self.layHWin.addWidget(self.lbl_stages) self.layHWin.addWidget(self.led_stages) self.layHWin.addStretch(1) self.layHWin.addWidget(self.chk_norm) self.layHWin.setContentsMargins(0,0,0,0) # Widget containing all subwidgets (cmbBoxes, Labels, lineEdits) self.wdg_fil = QWidget(self) self.wdg_fil.setObjectName('wdg_fil') self.wdg_fil.setLayout(self.layHWin) #---------------------------------------------------------------------- # SIGNALS & SLOTs #---------------------------------------------------------------------- self.led_delays.editingFinished.connect(self._update_UI) self.led_stages.editingFinished.connect(self._update_UI) # fires when edited line looses focus or when RETURN is pressed self.chk_norm.clicked.connect(self._update_UI) #---------------------------------------------------------------------- self.dict2filter_params() # get initial / last setting from dictionary self._update_UI()
[docs] def dict2filter_params(self): """ Reload parameter(s) from filter dictionary (if they exist) and set corresponding UI elements. load_dict() is called upon initialization and when the filter is loaded from disk. """ if 'filter_widgets' in fb.fil[0] and 'ma' in fb.fil[0]['filter_widgets']: wdg_fil_par = fb.fil[0]['filter_widgets']['ma'] if 'delays' in wdg_fil_par: self.delays = wdg_fil_par['delays'] self.led_delays.setText(str(self.delays)) if 'stages' in wdg_fil_par: self.stages = wdg_fil_par['stages'] self.led_stages.setText(str(self.stages)) if 'normalize' in wdg_fil_par: self.chk_norm.setChecked(wdg_fil_par['normalize'])
def _update_UI(self): """ Update UI when line edit field is changed (here, only the text is read and converted to integer) and resize the textfields according to content. """ self.delays = safe_eval(self.led_delays.text(), self.delays, return_type='int', sign='pos') self.led_delays.setText(str(self.delays)) self.stages = safe_eval(self.led_stages.text(), self.stages, return_type='int', sign='pos') self.led_stages.setText(str(self.stages)) self._store_entries() def _store_entries(self): """ Store parameter settings in filter dictionary. Called from _update_UI() and _save() """ fb.fil[0]['filter_widgets'].update({'ma': {'delays':self.delays, 'stages':self.stages, 'normalize':self.chk_norm.isChecked()} }) # sig_tx -> select_filter -> filter_specs self.emit({'filt_changed': 'ma'}) def _get_params(self, fil_dict): """ Translate parameters from the passed dictionary to instance parameters, scaling / transforming them if needed. """ # N is total order, L is number of taps per stage self.F_SB = fil_dict['F_SB'] self.A_SB = fil_dict['A_SB'] def _save(self, fil_dict): """ Save MA-filters both in 'zpk' and 'ba' format; no conversion has to be performed except maybe deleting an 'sos' entry from an earlier filter design. """ if 'zpk' in self.FRMT: fil_save(fil_dict, self.zpk, 'zpk', __name__, convert = False) if 'ba' in self.FRMT: fil_save(fil_dict, self.b, 'ba', __name__, convert = False) fil_convert(fil_dict, self.FRMT) # always update filter dict and LineEdit, in case the design algorithm # has changed the number of delays: fil_dict['N'] = self.delays * self.stages # updated filter order self.led_delays.setText(str(self.delays)) # updated number of delays self._store_entries()
[docs] def calc_ma(self, fil_dict, rt='LP'): """ Calculate coefficients and P/Z for moving average filter based on filter length L = N + 1 and number of cascaded stages and save the result in the filter dictionary. """ b = 1. k = 1. L = self.delays + 1 if rt == 'LP': b0 = np.ones(L) # h[n] = {1; 1; 1; ...} i = np.arange(1, L) norm = L elif rt == 'HP': b0 = np.ones(L) b0[::2] = -1. # h[n] = {1; -1; 1; -1; ...} i = np.arange(L) if (L % 2 == 0): # even order, remove middle element i = np.delete(i ,round(L/2.)) else: # odd order, shift by 0.5 and remove middle element i = np.delete(i, int(L/2.)) + 0.5 norm = L elif rt == 'BP': # N is even, L is odd b0 = np.ones(L) b0[1::2] = 0 b0[::4] = -1 # h[n] = {1; 0; -1; 0; 1; ... } L = L + 1 i = np.arange(L) # create N + 2 zeros around the unit circle, ... # ... remove first and middle element and rotate by L / 4 i = np.delete(i, [0, L // 2]) + L / 4 norm = np.sum(abs(b0)) elif rt == 'BS': # N is even, L is odd b0 = np.ones(L) b0[1::2] = 0 L = L + 1 i = np.arange(L) # create N + 2 zeros around the unit circle and ... i = np.delete(i, [0, L // 2]) # ... remove first and middle element norm = np.sum(b0) if self.delays > 1000: if not popup_warning(None, self.delays*self.stages, "Moving Average"): return -1 z0 = np.exp(-2j*np.pi*i/L) # calculate filter for multiple cascaded stages for i in range(self.stages): b = np.convolve(b0, b) z = np.repeat(z0, self.stages) # normalize filter to |H_max| = 1 if checked: if self.chk_norm.isChecked(): b = b / (norm ** self.stages) k = 1./norm ** self.stages p = np.zeros(len(z)) gain = zeros_with_val(len(z), k) # store in class attributes for the _save method self.zpk = np.array([z,p,gain]) self.b = b self._save(fil_dict)
[docs] def LPman(self, fil_dict): self._get_params(fil_dict) self.calc_ma(fil_dict, rt = 'LP')
[docs] def LPmin(self, fil_dict): self._get_params(fil_dict) self.delays = int(np.ceil(1 / (self.A_SB **(1/self.stages) * np.sin(self.F_SB * np.pi)))) self.calc_ma(fil_dict, rt = 'LP')
[docs] def HPman(self, fil_dict): self._get_params(fil_dict) self.calc_ma(fil_dict, rt = 'HP')
[docs] def HPmin(self, fil_dict): self._get_params(fil_dict) self.delays = int(np.ceil(1 / (self.A_SB **(1/self.stages) * np.sin((0.5 - self.F_SB) * np.pi)))) self.calc_ma(fil_dict, rt = 'HP')
[docs] def BSman(self, fil_dict): self._get_params(fil_dict) self.delays = ceil_odd(self.delays) # enforce odd order self.calc_ma(fil_dict, rt = 'BS')
[docs] def BPman(self, fil_dict): self._get_params(fil_dict) self.delays = ceil_odd(self.delays) # enforce odd order self.calc_ma(fil_dict, rt = 'BP')
#------------------------------------------------------------------------------ if __name__ == '__main__': import sys from pyfda.libs.compat import QApplication, QFrame app = QApplication(sys.argv) # instantiate filter widget filt = MA() filt.construct_UI() wdg_ma = getattr(filt, 'wdg_fil') layVDynWdg = QVBoxLayout() layVDynWdg.addWidget(wdg_ma, stretch = 1) filt.LPman(fb.fil[0]) # design a low-pass with parameters from global dict print(fb.fil[0]['zpk']) # return results in default format form = QFrame() form.setFrameStyle(QFrame.StyledPanel|QFrame.Sunken) form.setLayout(layVDynWdg) form.show() app.exec_() #------------------------------------------------------------------------------ # test using "python -m pyfda.filter_widgets.ma"