Exemple #1
0
class Delay(QWidget):

    FRMT = 'zpk' # output format of delay filter widget

    info ="""
**Delay widget**

allows entering the number of **delays** :math:`N` :math:`T_S`. It is treated as a FIR filter,
the number of delays is directly translated to a number of poles (:math:`N > 0`) 
or zeros (:math:`N < 0`).

Obviously, there is no minimum design algorithm or no design algorithm at all :-)

    """

    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.N = 5

        self.ft = 'FIR'
        
        self.rt_dicts = ('com',)

        self.rt_dict = {
            'COM': {'man': {'fo':('a', 'N'),
                            'msg':('a', 
                                "<span>Enter desired filter order <b><i>N</i></b>, corner "
                                "frequencies of pass and stop band(s), <b><i>F<sub>PB</sub></i></b>"
                                "&nbsp; and <b><i>F<sub>SB</sub></i></b>&nbsp;, and relative weight "
                                "values <b><i>W&nbsp; </i></b> (1 ... 10<sup>6</sup>) to specify how well "
                                "the bands are approximated.</span>")
                            },
                },
            'LP': {'man':{'wspecs': ('u','W_PB','W_SB'),
                          'tspecs': ('u', {'frq':('a','F_PB','F_SB'), 
                                           'amp':('u','A_PB','A_SB')})
                          },
                },
            'HP': {'man':{'wspecs': ('u','W_SB','W_PB')},
                    },
            'BP': {
                    },
            'BS': {'man':{'wspecs': ('u','W_PB','W_SB','W_PB2'),
                          'tspecs': ('u', {'frq':('a','F_PB','F_SB','F_SB2','F_PB2'), 
                                           'amp':('u','A_PB','A_SB','A_PB2')})
                          }
                }
            }

        self.info_doc = []

    #--------------------------------------------------------------------------
    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_delay = QLabel("Delays", self)
        self.lbl_delay.setObjectName('wdg_lbl_delays')
        self.led_delay = QLineEdit(self)
        self.led_delay.setText(str(self.N))
        self.led_delay.setObjectName('wdg_led_delay')
        self.led_delay.setToolTip("Number of delays, N > 0 produces poles, N < 0 zeros.")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_delay)
        self.layHWin.addWidget(self.led_delay)
        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_delay.editingFinished.connect(self._update_UI)
        # fires when edited line looses focus or when RETURN is pressed
        #----------------------------------------------------------------------

        self._load_dict() # get initial / last setting from dictionary
        self._update_UI()
        
    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and store parameter settings in filter 
        dictionary
        """
        self.N = safe_eval(self.led_delay.text(), self.N, 
                                      return_type='int')
        self.led_delay.setText(str(self.N))

        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil':{}})
        fb.fil[0]['wdg_fil'].update({'delay':
                                        {'N':self.N}
                                    })
        
        # sig_tx -> select_filter -> filter_specs   
        self.sig_tx.emit({'sender':__name__, 'filt_changed':'delay'})


    def _load_dict(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 'wdg_fil' in fb.fil[0] and 'delay' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['delay']
            if 'N' in wdg_fil_par:
                self.N = wdg_fil_par['N']
                self.led_delay.setText(str(self.N))


    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        self.N     = fil_dict['N']  # filter order is translated to numb. of delays
                                        
        
    def _test_N(self):
        """
        Warn the user if the calculated order is too high for a reasonable filter
        design.
        """
        if self.N > 2000:
            return qfilter_warning(self, self.N, "Delay")
        else:
            return True

    def _save(self, fil_dict, arg=None):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        if arg is None:
            arg = np.zeros(self.N)
        fil_save(fil_dict, arg, self.FRMT, __name__)

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)


    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)
        
    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(fil_dict)
Exemple #2
0
class AllpPZ(QWidget):

    FRMT = 'zpk'  # output format of delay filter widget

    info = """
**Allpass widget**

allows entering the two **poles** :math:`p`. **zeros** are calculated from the 
reciprocal values of the poles. There is no minimum algorithm, only the two 
poles can be entered manually.

    """

    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.p = [0.5, 0.5j]

        self.ft = 'IIR'

        # the following defines which subwidgets are "a"ctive, "i"nvisible or "d"eactivated
        self.rt_dicts = ('com', )
        self.rt_dict = {
            'COM': {
                'man': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "<span>Enter poles  <b><i>p</i></b> for allpass function,"
                     "zeros will be calculated.</span>")
                },
            },
            'AP': {
                'man': {}
            }
        }

        self.info_doc = []

    #--------------------------------------------------------------------------
    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_pole1 = QLabel("Pole 1", self)
        self.lbl_pole1.setObjectName('wdg_lbl_pole1')
        self.led_pole1 = QLineEdit(self)
        self.led_pole1.setText(str(self.p[0]))
        self.led_pole1.setObjectName('wdg_led_pole1')
        self.led_pole1.setToolTip("Pole 1 for allpass filter")

        self.lbl_pole2 = QLabel("Pole 2", self)
        self.lbl_pole2.setObjectName('wdg_lbl_pole2')
        self.led_pole2 = QLineEdit(self)
        self.led_pole2.setText(str(self.p[1]))
        self.led_pole2.setObjectName('wdg_led_pole2')
        self.led_pole2.setToolTip("Pole 2 for allpass filter")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_pole1)
        self.layHWin.addWidget(self.led_pole1)
        self.layHWin.addWidget(self.lbl_pole2)
        self.layHWin.addWidget(self.led_pole2)
        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_pole1.editingFinished.connect(self._update_UI)
        self.led_pole2.editingFinished.connect(self._update_UI)
        # fires when edited line looses focus or when RETURN is pressed
        #----------------------------------------------------------------------

        self._load_dict()  # get initial / last setting from dictionary
        self._update_UI()

    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and store parameter settings in filter 
        dictionary
        """
        self.p[0] = safe_eval(self.led_pole1.text(),
                              self.p[0],
                              return_type='cmplx')
        self.led_pole1.setText(str(self.p[0]))

        self.p[1] = safe_eval(self.led_pole2.text(),
                              self.p[1],
                              return_type='cmplx')
        self.led_pole2.setText(str(self.p[1]))

        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update(
            {'poles': {
                'p1': self.p[0],
                'p2': self.p[1]
            }})

        # sig_tx -> select_filter -> filter_specs
        self.sig_tx.emit({'sender': __name__, 'filt_changed': 'pole_1_2'})

    def _load_dict(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 'wdg_fil' in fb.fil[0] and 'poles' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['poles']
            if 'p1' in wdg_fil_par:
                self.p1 = wdg_fil_par['p1']
                self.led_pole1.setText(str(self.p1))
            if 'p2' in wdg_fil_par:
                self.p2 = wdg_fil_par['p2']
                self.led_pole2.setText(str(self.p2))

    def _get_params(self, fil_dict):
        """
        Get parameters needed for filter design from the passed dictionary and 
        translate them to instance parameters, scaling / transforming them if needed.
        """
        #self.p1     = fil_dict['zpk'][1][0]  # get the first and second pole
        #self.p2     = fil_dict['zpk'][1][1]  # from central filter dect
        logger.info(fil_dict['zpk'])

    def _test_poles(self):
        """
        Warn the user if one of the poles is outside the unit circle
        """
        if abs(self.p[0]) >= 1 or abs(self.p[1]) >= 1:
            return qfilter_warning(self, self.p[0], "Delay")
        else:
            return True

    def _save(self, fil_dict, arg=None):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        if arg is None:
            logger.error("Passed empty filter dict")
        logger.info(arg)
        fil_save(fil_dict, arg, self.FRMT, __name__)

        fil_dict['N'] = len(self.p)

#------------------------------------------------------------------------------
# Filter design routines
#------------------------------------------------------------------------------
# The method name MUST be "FilterType"+"MinMan", e.g. LPmin or BPman

    def APman(self, fil_dict):
        """
        Calculate z =1/p* for a given set of poles p. If p=0, set z=0.
        The gain factor k is calculated from z and p at z = 1.
        """
        self._get_params(fil_dict)  # not needed here
        if not self._test_poles():
            return -1
        self.z = [0, 0]
        if self.p[0] != 0:
            self.z[0] = np.conj(1 / self.p[0])
        if type(self.p[0]) == complex:
            pass
        if self.p[1] != 0:
            self.z[1] = np.conj(1 / self.p[1])

        k = np.abs(
            np.polyval(np.poly(self.p), 1) / np.polyval(np.poly(self.z), 1))
        zpk_list = [self.z, self.p, k]

        self._save(fil_dict, zpk_list)
Exemple #3
0
class EllipZeroPhz(QWidget):

#    Since we are also using poles/residues -> let's force zpk
#    if SOS_AVAIL:
#       output format of filter design routines 'zpk' / 'ba' / 'sos'
#        FRMT = 'sos' 
#    else:
    FRMT = 'zpk'
        
    info = """
**Elliptic filters with zero phase**

(also known as Cauer filters) have the steepest rate of transition between the 
frequency response’s passband and stopband of all IIR filters. This comes
at the expense of a constant ripple (equiripple) :math:`A_PB` and :math:`A_SB`
in both pass and stop band. Ringing of the step response is increased in
comparison to Chebychev filters.
 
As the passband ripple :math:`A_PB` approaches 0, the elliptical filter becomes
a Chebyshev type II filter. As the stopband ripple :math:`A_SB` approaches 0,
it becomes a Chebyshev type I filter. As both approach 0, becomes a Butterworth
filter (butter).

For the filter design, the order :math:`N`, minimum stopband attenuation
:math:`A_SB` and the critical frequency / frequencies :math:`F_PB` where the 
gain first drops below the maximum passband ripple :math:`-A_PB` have to be specified.

The ``ellipord()`` helper routine calculates the minimum order :math:`N` and 
critical passband frequency :math:`F_C` from pass and stop band specifications.

The Zero Phase Elliptic Filter squares an elliptic filter designed in
a way to produce the required Amplitude specifications. So initially the
amplitude specs design an elliptic filter with the square root of the amp specs.
The filter is then squared to produce a zero phase filter.
The filter coefficients are applied to the signal data in a backward and forward
time fashion.  This filter can only be applied to stored signal data (not
real-time streaming data that comes in a forward time order).

We are forcing the order N of the filter to be even.  This simplifies the poles/zeros
to be complex (no real values).

**Design routines:**

``scipy.signal.ellip()``, ``scipy.signal.ellipord()``

        """
    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.ft = 'IIR'

        c = Common()
        self.rt_dict = c.rt_base_iir

        self.rt_dict_add = {
            'COM':{'man':{'msg':('a',
             "Enter the filter order <b><i>N</i></b>, the minimum stop "
             "band attenuation <b><i>A<sub>SB</sub></i></b> and frequency or "
             "frequencies <b><i>F<sub>C</sub></i></b>  where gain first drops "
             "below the max passband ripple <b><i>-A<sub>PB</sub></i></b> .")}},
            'LP': {'man':{}, 'min':{}},
            'HP': {'man':{}, 'min':{}},
            'BS': {'man':{}, 'min':{}},
            'BP': {'man':{}, 'min':{}},
            }

        self.info_doc = []
        self.info_doc.append('ellip()\n========')
        self.info_doc.append(sig.ellip.__doc__)
        self.info_doc.append('ellipord()\n==========')
        self.info_doc.append(ellipord.__doc__)

    #--------------------------------------------------------------------------
    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.chkComplex   = QCheckBox("ComplexFilter", self)
        self.chkComplex.setToolTip("Designs BP or BS Filter for complex data.")
        self.chkComplex.setObjectName('wdg_lbl_el')
        self.chkComplex.setChecked(False)

        #--------------------------------------------------
        #  Layout for filter optional subwidgets
        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        #self.layHWin.addWidget(self.chkComplex)
        self.layHWin.addStretch() 
        self.layHWin.setContentsMargins(0,0,0,0)

        # Widget containing all subwidgets
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        For zero phase filter, we take square root of amplitude specs
        since we later square filter.  Define design around smallest amp spec
        """
        # Frequencies are normalized to f_Nyq = f_S/2, ripple specs are in dB
        self.analog = False # set to True for analog filters
        self.manual = False # default is normal design
        self.N     = int(fil_dict['N'])

        # force N to be even
        if (self.N % 2) == 1:
            self.N += 1
        self.F_PB  = fil_dict['F_PB'] * 2
        self.F_SB  = fil_dict['F_SB'] * 2
        self.F_PB2 = fil_dict['F_PB2'] * 2
        self.F_SB2 = fil_dict['F_SB2'] * 2
        self.F_PBC = None

        # find smallest spec'd linear value and rewrite dictionary
        ampPB = fil_dict['A_PB']
        ampSB = fil_dict['A_SB']

        # take square roots of amp specs so resulting squared
        # filter will meet specifications
        if (ampPB < ampSB):
            ampSB = sqrt(ampPB)
            ampPB = sqrt(1+ampPB)-1
        else:
            ampPB = sqrt(1+ampSB)-1
            ampSB = sqrt(ampSB)
        self.A_PB = lin2unit(ampPB, 'IIR', 'A_PB', unit='dB')
        self.A_SB = lin2unit(ampSB, 'IIR', 'A_SB', unit='dB')
        #logger.warning("design with "+str(self.A_PB)+","+str(self.A_SB))

        # ellip filter routines support only one amplitude spec for
        # pass- and stop band each
        if str(fil_dict['rt']) == 'BS':
            fil_dict['A_PB2'] = self.A_PB
        elif str(fil_dict['rt']) == 'BP':
            fil_dict['A_SB2'] = self.A_SB

#   partial fraction expansion to define residue vector
    def _partial(self, k, p, z, norder):
        # create diff array
        diff = p - z

        # now compute residual vector
        cone = complex(1.,0.)
        residues = zeros(norder, complex)
        for i in range(norder):
            residues[i] =  k * (diff[i] / p[i])
            for j in range(norder):
                if (j != i):
                    residues[i] = residues[i] * (cone + diff[j]/(p[i] - p[j]))

        # now compute DC term for new expansion
        sumRes = 0.
        for i in range(norder):
            sumRes = sumRes + residues[i].real

        dc = k - sumRes

        return (dc, residues)

#
# Take a causal filter and square it. The result has the square
#  of the amplitude response of the input, and zero phase. Filter
#  is noncausal.
# Input:
#   k - gain in pole/zero form
#   p - numpy array of poles
#   z - numpy array of zeros
#   g - gain in pole/residue form
#   r - numpy array of residues
#   nn- order of filter

# Output:
#   kn - new gain (pole/zero)
#   pn - new poles
#   zn - new zeros  (numpy array)
#   gn - new gain (pole/residue)
#   rn - new residues

    def _sqCausal (self, k, p, z, g, r, nn):

#       Anticausal poles have conjugate-reciprocal symmetry
#       Starting anticausal residues are conjugates (adjusted below)

        pA = conj(1./p)   # antiCausal poles
        zA = conj(z)      # antiCausal zeros (store reciprocal)
        rA = conj(r)      # antiCausal residues (to start)
        rC = zeros(nn, complex)

#       Adjust residues. Causal part first.
        for j in range(nn):

#           Evaluate the anticausal filter at each causal pole
            tmpx = rA / (1. - p[j]/pA)
            ztmp = g + sum(tmpx)

#           Adjust residue
            rC[j] = r[j]*ztmp

#       anticausal residues are just conjugates of causal residues
#        r3 = np.conj(r2)

#       Compute the constant term
        dc2 = (g + sum(r))*g - sum(rC)

#       Populate output (2nn elements)
        gn = dc2.real

#       Drop complex poles/residues in LHP, keep only UHP

        pA = conj(p)  #store AntiCasual pole (reciprocal)
        p0 = zeros(int(nn/2), complex)
        r0 = zeros(int(nn/2), complex)
        cnt = 0
        for j in range(nn):
            if (p[j].imag > 0.0):
                p0[cnt] = p[j]
                r0[cnt] = rC[j]
                cnt = cnt+1

#       Let operator know we squared filter
#        logger.info('After squaring filter, order: '+str(nn*2))

#       For now and our case, only store causal residues
#       Filters are symmetric and can generate antiCausal residues
        return (pA, zA, gn, p0, r0)


    def _test_N(self):
        """
        Warn the user if the calculated order is too high for a reasonable filter
        design.
        """
        if self.N > 30:
            return qfilter_warning(self, self.N, "Zero-phase Elliptic")
        else:
            return True

#   custom save of filter dictionary
    def _save(self, fil_dict, arg):
        """
        First design initial elliptic filter meeting sqRoot Amp specs;
         - Then create residue vector from poles/zeros;
         - Then square filter (k,p,z and dc,p,r) to get zero phase filter;
         - Then Convert results of filter design to all available formats (pz, pr, ba, sos)
        and store them in the global filter dictionary.

        Corner frequencies and order calculated for minimum filter order are
        also stored to allow for an easy subsequent manual filter optimization.
        """
        fil_save(fil_dict, arg, self.FRMT, __name__)

        # For min. filter order algorithms, update filter dict with calculated
        # new values for filter order N and corner frequency(s) F_PBC

        fil_dict['N'] = self.N
        if str(fil_dict['fo']) == 'min':
            if str(fil_dict['rt']) == 'LP' or str(fil_dict['rt']) == 'HP':
#               HP or LP - single  corner frequency
                fil_dict['F_PB'] = self.F_PBC / 2.
            else: # BP or BS - two corner frequencies
                fil_dict['F_PB'] = self.F_PBC[0] / 2.
                fil_dict['F_PB2'] = self.F_PBC[1] / 2.

#       Now generate poles/residues for custom file save of new parameters
        if (not self.manual):
            z = fil_dict['zpk'][0]
            p = fil_dict['zpk'][1]
            k = fil_dict['zpk'][2]
            n = len(z)
            gain, residues = self._partial (k, p, z, n)

            pA, zA, gn, pC, rC = self._sqCausal (k, p, z, gain, residues, n)
            fil_dict['rpk'] = [rC, pC, gn]

#           save antiCausal b,a (nonReciprocal) also [easier to compute h(n)
            try:
               fil_dict['baA'] = sig.zpk2tf(zA, pA, k)
            except Exception as e:
               logger.error(e)

#       'rpk' is our signal that this is a non-Causal filter with zero phase
#       inserted into fil dictionary after fil_save and convert
        # sig_tx -> select_filter -> filter_specs   
        self.sig_tx.emit({'sender':__name__, 'filt_changed':'ellip_zero'})

#------------------------------------------------------------------------------
#
#         DESIGN ROUTINES
#
#------------------------------------------------------------------------------

    # LP: F_PB < F_stop -------------------------------------------------------
    def LPmin(self, fil_dict):
        """Elliptic LP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,self.F_SB, self.A_PB,self.A_SB,                                                        analog=self.analog)
#       force even N
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1              
        #logger.warning("and "+str(self.F_PBC) + " " + str(self.N))
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                            btype='low', analog=self.analog, output=self.FRMT))

    def LPman(self, fil_dict):
        """Elliptic LP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PB,
                            btype='low', analog=self.analog, output=self.FRMT))

    # HP: F_stop < F_PB -------------------------------------------------------
    def HPmin(self, fil_dict):
        """Elliptic HP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,self.F_SB, self.A_PB,self.A_SB,
                                                          analog=self.analog)
#       force even N
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                        btype='highpass', analog=self.analog, output=self.FRMT))

    def HPman(self, fil_dict):
        """Elliptic HP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PB,
                        btype='highpass', analog=self.analog, output=self.FRMT))

    # For BP and BS, F_XX have two elements each, A_XX has only one

    # BP: F_SB[0] < F_PB[0], F_SB[1] > F_PB[1] --------------------------------
    def BPmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
            [self.F_SB, self.F_SB2], self.A_PB, self.A_SB, analog=self.analog)
        #logger.warning(" "+str(self.F_PBC) + " " + str(self.N))
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1  
        #logger.warning("-"+str(self.F_PBC) + " " + str(self.A_SB))
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                        btype='bandpass', analog=self.analog, output=self.FRMT))

    def BPman(self, fil_dict):
        """Elliptic BP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB,
            [self.F_PB,self.F_PB2], btype='bandpass', analog=self.analog,
                                                            output=self.FRMT))

    # BS: F_SB[0] > F_PB[0], F_SB[1] < F_PB[1] --------------------------------
    def BSmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
                                [self.F_SB, self.F_SB2], self.A_PB,self.A_SB,                                                       analog=self.analog)
#       force even N
        if (self.N%2)== 1:
            self.N += 1
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB, self.F_PBC,
                        btype='bandstop', analog=self.analog, output=self.FRMT))

    def BSman(self, fil_dict):
        """Elliptic BS filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1  
        self._save(fil_dict, sig.ellip(self.N, self.A_PB, self.A_SB,
            [self.F_PB,self.F_PB2], btype='bandstop', analog=self.analog,
                                                            output=self.FRMT))
Exemple #4
0
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):
        QWidget.__init__(self)

        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__)

#--------------------------------------------------------------------------

    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._load_dict()  # get initial / last setting from dictionary
        self._update_UI()

    def _load_dict(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 'wdg_fil' in fb.fil[0] and 'ma' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['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()
        """
        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].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()

    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 qfilter_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))

        # store in class attributes for the _save method
        self.zpk = [z, p, k]
        self.b = b
        self._save(fil_dict)

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        self.calc_ma(fil_dict, rt='LP')

    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')

    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        self.calc_ma(fil_dict, rt='HP')

    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')

    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')

    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')
Exemple #5
0
class Equiripple(QWidget):

    FRMT = 'ba'  # output format of filter design routines 'zpk' / 'ba' / 'sos'
    # currently, only 'ba' is supported for equiripple routines

    info = """
**Equiripple filters**

have the steepest rate of transition between the frequency response’s passband
and stopband of all FIR filters. This comes at the expense of a constant ripple
(equiripple) :math:`A_PB` and :math:`A_SB` in both pass and stop band.

The filter-coefficients are calculated in such a way that the transfer function
minimizes the maximum error (**Minimax** design) between the desired gain and the
realized gain in the specified frequency bands using the **Remez** exchange algorithm.
The filter design algorithm is known as **Parks-McClellan** algorithm, in
Matlab (R) it is called ``firpm``.

Manual filter order design requires specifying the frequency bands (:math:`F_PB`,
:math:`f_SB` etc.), the filter order :math:`N` and weight factors :math:`W_PB`,
:math:`W_SB` etc.) for individual bands.

The minimum order and the weight factors needed to fulfill the target specifications
is estimated from frequency and amplitude specifications using Ichige's algorithm.

**Design routines:**

``scipy.signal.remez()``, ``pyfda_lib.remezord()``
    """

    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.grid_density = 16

        self.ft = 'FIR'

        self.rt_dicts = ('com', )

        self.rt_dict = {
            'COM': {
                'man': {
                    'fo': ('a', 'N'),
                    'msg':
                    ('a',
                     "<span>Enter desired filter order <b><i>N</i></b>, corner "
                     "frequencies of pass and stop band(s), <b><i>F<sub>PB</sub></i></b>"
                     "&nbsp; and <b><i>F<sub>SB</sub></i></b>&nbsp;, and relative weight "
                     "values <b><i>W&nbsp; </i></b> (1 ... 10<sup>6</sup>) to specify how well "
                     "the bands are approximated.</span>")
                },
                'min': {
                    'fo': ('d', 'N'),
                    'msg':
                    ('a',
                     "<span>Enter the maximum pass band ripple <b><i>A<sub>PB</sub></i></b>, "
                     "minimum stop band attenuation <b><i>A<sub>SB</sub></i></b> "
                     "and the corresponding corner frequencies of pass and "
                     "stop band(s), <b><i>F<sub>PB</sub></i></b>&nbsp; and "
                     "<b><i>F<sub>SB</sub></i></b> .</span>")
                }
            },
            'LP': {
                'man': {
                    'wspecs': ('a', 'W_PB', 'W_SB'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_PB', 'F_SB'),
                        'amp': ('u', 'A_PB', 'A_SB')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_PB', 'W_SB'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_PB', 'F_SB'),
                        'amp': ('a', 'A_PB', 'A_SB')
                    })
                }
            },
            'HP': {
                'man': {
                    'wspecs': ('a', 'W_SB', 'W_PB'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_SB', 'F_PB'),
                        'amp': ('u', 'A_SB', 'A_PB')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_SB', 'W_PB'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_SB', 'F_PB'),
                        'amp': ('a', 'A_SB', 'A_PB')
                    })
                }
            },
            'BP': {
                'man': {
                    'wspecs': ('a', 'W_SB', 'W_PB', 'W_SB2'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_SB', 'F_PB', 'F_PB2', 'F_SB2'),
                        'amp': ('u', 'A_SB', 'A_PB', 'A_SB2')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_SB', 'W_PB', 'W_SB2'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_SB', 'F_PB', 'F_PB2', 'F_SB2'),
                        'amp': ('a', 'A_SB', 'A_PB', 'A_SB2')
                    })
                },
            },
            'BS': {
                'man': {
                    'wspecs': ('a', 'W_PB', 'W_SB', 'W_PB2'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_PB', 'F_SB', 'F_SB2', 'F_PB2'),
                        'amp': ('u', 'A_PB', 'A_SB', 'A_PB2')
                    })
                },
                'min': {
                    'wspecs': ('d', 'W_PB', 'W_SB', 'W_PB2'),
                    'tspecs': ('a', {
                        'frq': ('a', 'F_PB', 'F_SB', 'F_SB2', 'F_PB2'),
                        'amp': ('a', 'A_PB', 'A_SB', 'A_PB2')
                    })
                }
            },
            'HIL': {
                'man': {
                    'wspecs': ('a', 'W_SB', 'W_PB', 'W_SB2'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_SB', 'F_PB', 'F_PB2', 'F_SB2'),
                        'amp': ('u', 'A_SB', 'A_PB', 'A_SB2')
                    })
                }
            },
            'DIFF': {
                'man': {
                    'wspecs': ('a', 'W_PB'),
                    'tspecs': ('u', {
                        'frq': ('a', 'F_PB'),
                        'amp': ('i', )
                    }),
                    'msg':
                    ('a',
                     "Enter the max. frequency up to where the differentiator "
                     "works.")
                }
            }
        }

        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__)

    #--------------------------------------------------------------------------
    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_remez_1 = QLabel("Grid Density", self)
        self.lbl_remez_1.setObjectName('wdg_lbl_remez_1')
        self.led_remez_1 = QLineEdit(self)
        self.led_remez_1.setText(str(self.grid_density))
        self.led_remez_1.setObjectName('wdg_led_remez_1')
        self.led_remez_1.setToolTip(
            "Number of frequency points for Remez algorithm. Increase the\n"
            "number to reduce frequency overshoot in the transition region.")

        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        self.layHWin.addWidget(self.lbl_remez_1)
        self.layHWin.addWidget(self.led_remez_1)
        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_remez_1.editingFinished.connect(self._update_UI)
        # fires when edited line looses focus or when RETURN is pressed
        #----------------------------------------------------------------------

        self._load_dict()  # get initial / last setting from dictionary
        self._update_UI()

    def _update_UI(self):
        """
        Update UI when line edit field is changed (here, only the text is read
        and converted to integer) and store parameter settings in filter 
        dictionary
        """
        self.grid_density = safe_eval(self.led_remez_1.text(),
                                      self.grid_density,
                                      return_type='int',
                                      sign='pos')
        self.led_remez_1.setText(str(self.grid_density))

        if not 'wdg_fil' in fb.fil[0]:
            fb.fil[0].update({'wdg_fil': {}})
        fb.fil[0]['wdg_fil'].update(
            {'equiripple': {
                'grid_density': self.grid_density
            }})

        # sig_tx -> select_filter -> filter_specs
        self.sig_tx.emit({'sender': __name__, 'filt_changed': 'equiripple'})

    def _load_dict(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 'wdg_fil' in fb.fil[0] and 'equiripple' in fb.fil[0]['wdg_fil']:
            wdg_fil_par = fb.fil[0]['wdg_fil']['equiripple']
            if 'grid_density' in wdg_fil_par:
                self.grid_density = wdg_fil_par['grid_density']
                self.led_remez_1.setText(str(self.grid_density))

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        """
        self.N = fil_dict['N'] + 1  # remez algorithms expects number of taps
        # which is larger by one than the order!!
        self.F_PB = fil_dict['F_PB']
        self.F_SB = fil_dict['F_SB']
        self.F_PB2 = fil_dict['F_PB2']
        self.F_SB2 = fil_dict['F_SB2']
        # remez amplitude specs are linear (not in dBs)
        self.A_PB = fil_dict['A_PB']
        self.A_PB2 = fil_dict['A_PB2']
        self.A_SB = fil_dict['A_SB']
        self.A_SB2 = fil_dict['A_SB2']

        self.alg = 'ichige'

    def _test_N(self):
        """
        Warn the user if the calculated order is too high for a reasonable filter
        design.
        """
        if self.N > 2000:
            return qfilter_warning(self, self.N, "Equiripple")
        else:
            return True

    def _save(self, fil_dict, arg):
        """
        Convert between poles / zeros / gain, filter coefficients (polynomes)
        and second-order sections and store all available formats in the passed
        dictionary 'fil_dict'.
        """
        try:
            fil_save(fil_dict, arg, self.FRMT, __name__)
        except Exception as e:
            # catch exception due to malformatted coefficients:
            logger.error("While saving the equiripple filter design, "
                         "the following error occurred:\n{0}".format(e))
            return -1

        if str(fil_dict['fo']) == 'min':
            fil_dict['N'] = self.N - 1  # yes, update filterbroker

    def LPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(self.N, [0, self.F_PB, self.F_SB, 0.5], [1, 0],
                      weight=[fil_dict['W_PB'], fil_dict['W_SB']],
                      fs=1,
                      grid_density=self.grid_density))

    def LPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A, W) = remezord([self.F_PB, self.F_SB], [1, 0],
                                     [self.A_PB, self.A_SB],
                                     fs=1,
                                     alg=self.alg)
        if not self._test_N():
            return -1
        fil_dict['W_PB'] = W[0]
        fil_dict['W_SB'] = W[1]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [1, 0],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def HPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        if (self.N % 2 == 0):  # even order, use odd symmetry (type III)
            self._save(
                fil_dict,
                sig.remez(self.N, [0, self.F_SB, self.F_PB, 0.5], [0, 1],
                          weight=[fil_dict['W_SB'], fil_dict['W_PB']],
                          fs=1,
                          type='hilbert',
                          grid_density=self.grid_density))
        else:  # odd order,
            self._save(
                fil_dict,
                sig.remez(self.N, [0, self.F_SB, self.F_PB, 0.5], [0, 1],
                          weight=[fil_dict['W_SB'], fil_dict['W_PB']],
                          fs=1,
                          type='bandpass',
                          grid_density=self.grid_density))

    def HPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A, W) = remezord([self.F_SB, self.F_PB], [0, 1],
                                     [self.A_SB, self.A_PB],
                                     fs=1,
                                     alg=self.alg)
        if not self._test_N():
            return -1
#        self.N = ceil_odd(N)  # enforce odd order
        fil_dict['W_SB'] = W[0]
        fil_dict['W_PB'] = W[1]
        if (self.N % 2 == 0):  # even order
            self._save(
                fil_dict,
                sig.remez(self.N,
                          F, [0, 1],
                          weight=W,
                          fs=1,
                          type='hilbert',
                          grid_density=self.grid_density))
        else:
            self._save(
                fil_dict,
                sig.remez(self.N,
                          F, [0, 1],
                          weight=W,
                          fs=1,
                          type='bandpass',
                          grid_density=self.grid_density))

    # For BP and BS, F_PB and F_SB have two elements each
    def BPman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_SB, self.F_PB, self.F_PB2, self.F_SB2, 0.5],
                [0, 1, 0],
                weight=[fil_dict['W_SB'], fil_dict['W_PB'], fil_dict['W_SB2']],
                fs=1,
                grid_density=self.grid_density))

    def BPmin(self, fil_dict):
        self._get_params(fil_dict)
        (self.N, F, A,
         W) = remezord([self.F_SB, self.F_PB, self.F_PB2, self.F_SB2],
                       [0, 1, 0], [self.A_SB, self.A_PB, self.A_SB2],
                       fs=1,
                       alg=self.alg)
        if not self._test_N():
            return -1
        fil_dict['W_SB'] = W[0]
        fil_dict['W_PB'] = W[1]
        fil_dict['W_SB2'] = W[2]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [0, 1, 0],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def BSman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.N = round_odd(self.N)  # enforce odd order
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_PB, self.F_SB, self.F_SB2, self.F_PB2, 0.5],
                [1, 0, 1],
                weight=[fil_dict['W_PB'], fil_dict['W_SB'], fil_dict['W_PB2']],
                fs=1,
                grid_density=self.grid_density))

    def BSmin(self, fil_dict):
        self._get_params(fil_dict)
        (N, F, A, W) = remezord([self.F_PB, self.F_SB, self.F_SB2, self.F_PB2],
                                [1, 0, 1], [self.A_PB, self.A_SB, self.A_PB2],
                                fs=1,
                                alg=self.alg)
        self.N = round_odd(N)  # enforce odd order
        if not self._test_N():
            return -1
        fil_dict['W_PB'] = W[0]
        fil_dict['W_SB'] = W[1]
        fil_dict['W_PB2'] = W[2]
        self._save(
            fil_dict,
            sig.remez(self.N,
                      F, [1, 0, 1],
                      weight=W,
                      fs=1,
                      grid_density=self.grid_density))

    def HILman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.remez(
                self.N, [0, self.F_SB, self.F_PB, self.F_PB2, self.F_SB2, 0.5],
                [0, 1, 0],
                weight=[fil_dict['W_SB'], fil_dict['W_PB'], fil_dict['W_SB2']],
                fs=1,
                type='hilbert',
                grid_density=self.grid_density))

    def DIFFman(self, fil_dict):
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self.N = ceil_even(self.N)  # enforce even order
        if self.F_PB < 0.1:
            logger.warning(
                "Bandwidth for pass band ({0}) is too low, inreasing to 0.1".
                format(self.F_PB))
            self.F_PB = 0.1
            fil_dict['F_PB'] = self.F_PB
            self.sig_tx.emit({
                'sender': __name__,
                'specs_changed': 'equiripple'
            })

        self._save(
            fil_dict,
            sig.remez(self.N, [0, self.F_PB], [np.pi * fil_dict['W_PB']],
                      fs=1,
                      type='differentiator',
                      grid_density=self.grid_density))
Exemple #6
0
class EllipZeroPhz(QWidget):

    #    Since we are also using poles/residues -> let's force zpk
    FRMT = 'zpk'

    info = """
**Elliptic filters with zero phase**

(also known as Cauer filters) have the steepest rate of transition between the 
frequency response’s passband and stopband of all IIR filters. This comes
at the expense of a constant ripple (equiripple) :math:`A_PB` and :math:`A_SB`
in both pass and stop band. Ringing of the step response is increased in
comparison to Chebychev filters.
 
As the passband ripple :math:`A_PB` approaches 0, the elliptical filter becomes
a Chebyshev type II filter. As the stopband ripple :math:`A_SB` approaches 0,
it becomes a Chebyshev type I filter. As both approach 0, becomes a Butterworth
filter (butter).

For the filter design, the order :math:`N`, minimum stopband attenuation
:math:`A_SB` and the critical frequency / frequencies :math:`F_PB` where the 
gain first drops below the maximum passband ripple :math:`-A_PB` have to be specified.

The ``ellipord()`` helper routine calculates the minimum order :math:`N` and 
critical passband frequency :math:`F_C` from pass and stop band specifications.

The Zero Phase Elliptic Filter squares an elliptic filter designed in
a way to produce the required Amplitude specifications. So initially the
amplitude specs design an elliptic filter with the square root of the amp specs.
The filter is then squared to produce a zero phase filter.
The filter coefficients are applied to the signal data in a backward and forward
time fashion.  This filter can only be applied to stored signal data (not
real-time streaming data that comes in a forward time order).

We are forcing the order N of the filter to be even.  This simplifies the poles/zeros
to be complex (no real values).

**Design routines:**

``scipy.signal.ellip()``, ``scipy.signal.ellipord()``

        """
    sig_tx = pyqtSignal(object)

    def __init__(self):
        QWidget.__init__(self)

        self.ft = 'IIR'

        c = Common()
        self.rt_dict = c.rt_base_iir

        self.rt_dict_add = {
            'COM': {
                'man': {
                    'msg':
                    ('a',
                     "Enter the filter order <b><i>N</i></b>, the minimum stop "
                     "band attenuation <b><i>A<sub>SB</sub></i></b> and frequency or "
                     "frequencies <b><i>F<sub>C</sub></i></b>  where gain first drops "
                     "below the max passband ripple <b><i>-A<sub>PB</sub></i></b> ."
                     )
                }
            },
            'LP': {
                'man': {},
                'min': {}
            },
            'HP': {
                'man': {},
                'min': {}
            },
            'BS': {
                'man': {},
                'min': {}
            },
            'BP': {
                'man': {},
                'min': {}
            },
        }

        self.info_doc = []
        self.info_doc.append('ellip()\n========')
        self.info_doc.append(sig.ellip.__doc__)
        self.info_doc.append('ellipord()\n==========')
        self.info_doc.append(ellipord.__doc__)

    #--------------------------------------------------------------------------
    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.chkComplex   = QCheckBox("ComplexFilter", self)
        #         self.chkComplex.setToolTip("Designs BP or BS Filter for complex data.")
        #         self.chkComplex.setObjectName('wdg_lbl_el')
        #         self.chkComplex.setChecked(False)
        #
        # =============================================================================
        self.butSave = QPushButton(self)
        self.butSave.setText("SAVE")
        self.butSave.setToolTip("Save filter in proprietary format")

        #--------------------------------------------------
        #  Layout for filter optional subwidgets
        self.layHWin = QHBoxLayout()
        self.layHWin.setObjectName('wdg_layGWin')
        #self.layHWin.addWidget(self.chkComplex)
        self.layHWin.addWidget(self.butSave)
        self.layHWin.addStretch()
        self.layHWin.setContentsMargins(0, 0, 0, 0)

        # Widget containing all subwidgets
        self.wdg_fil = QWidget(self)
        self.wdg_fil.setObjectName('wdg_fil')
        self.wdg_fil.setLayout(self.layHWin)

        self.butSave.clicked.connect(self.save_filter)

    def _get_params(self, fil_dict):
        """
        Translate parameters from the passed dictionary to instance
        parameters, scaling / transforming them if needed.
        For zero phase filter, we take square root of amplitude specs
        since we later square filter.  Define design around smallest amp spec
        """
        # Frequencies are normalized to f_Nyq = f_S/2, ripple specs are in dB
        self.analog = False  # set to True for analog filters
        self.manual = False  # default is normal design
        self.N = int(fil_dict['N'])

        # force N to be even
        if (self.N % 2) == 1:
            self.N += 1
        self.F_PB = fil_dict['F_PB'] * 2
        self.F_SB = fil_dict['F_SB'] * 2
        self.F_PB2 = fil_dict['F_PB2'] * 2
        self.F_SB2 = fil_dict['F_SB2'] * 2
        self.F_PBC = None

        # find smallest spec'd linear value and rewrite dictionary
        ampPB = fil_dict['A_PB']
        ampSB = fil_dict['A_SB']

        # take square roots of amp specs so resulting squared
        # filter will meet specifications
        if (ampPB < ampSB):
            ampSB = sqrt(ampPB)
            ampPB = sqrt(1 + ampPB) - 1
        else:
            ampPB = sqrt(1 + ampSB) - 1
            ampSB = sqrt(ampSB)
        self.A_PB = lin2unit(ampPB, 'IIR', 'A_PB', unit='dB')
        self.A_SB = lin2unit(ampSB, 'IIR', 'A_SB', unit='dB')
        #logger.warning("design with "+str(self.A_PB)+","+str(self.A_SB))

        # ellip filter routines support only one amplitude spec for
        # pass- and stop band each
        if str(fil_dict['rt']) == 'BS':
            fil_dict['A_PB2'] = self.A_PB
        elif str(fil_dict['rt']) == 'BP':
            fil_dict['A_SB2'] = self.A_SB

#   partial fraction expansion to define residue vector

    def _partial(self, k, p, z, norder):
        # create diff array
        diff = p - z

        # now compute residual vector
        cone = complex(1., 0.)
        residues = zeros(norder, complex)
        for i in range(norder):
            residues[i] = k * (diff[i] / p[i])
            for j in range(norder):
                if (j != i):
                    residues[i] = residues[i] * (cone + diff[j] /
                                                 (p[i] - p[j]))

        # now compute DC term for new expansion
        sumRes = 0.
        for i in range(norder):
            sumRes = sumRes + residues[i].real

        dc = k - sumRes

        return (dc, residues)

#
# Take a causal filter and square it. The result has the square
#  of the amplitude response of the input, and zero phase. Filter
#  is noncausal.
# Input:
#   k - gain in pole/zero form
#   p - numpy array of poles
#   z - numpy array of zeros
#   g - gain in pole/residue form
#   r - numpy array of residues
#   nn- order of filter

# Output:
#   kn - new gain (pole/zero)
#   pn - new poles
#   zn - new zeros  (numpy array)
#   gn - new gain (pole/residue)
#   rn - new residues

    def _sqCausal(self, k, p, z, g, r, nn):

        #       Anticausal poles have conjugate-reciprocal symmetry
        #       Starting anticausal residues are conjugates (adjusted below)

        pA = conj(1. / p)  # antiCausal poles
        zA = conj(z)  # antiCausal zeros (store reciprocal)
        rA = conj(r)  # antiCausal residues (to start)
        rC = zeros(nn, complex)

        #       Adjust residues. Causal part first.
        for j in range(nn):

            #           Evaluate the anticausal filter at each causal pole
            tmpx = rA / (1. - p[j] / pA)
            ztmp = g + sum(tmpx)

            #           Adjust residue
            rC[j] = r[j] * ztmp

#       anticausal residues are just conjugates of causal residues
#        r3 = np.conj(r2)

#       Compute the constant term
        dc2 = (g + sum(r)) * g - sum(rC)

        #       Populate output (2nn elements)
        gn = dc2.real

        #       Drop complex poles/residues in LHP, keep only UHP

        pA = conj(p)  #store AntiCasual pole (reciprocal)
        p0 = zeros(int(nn / 2), complex)
        r0 = zeros(int(nn / 2), complex)
        cnt = 0
        for j in range(nn):
            if (p[j].imag > 0.0):
                p0[cnt] = p[j]
                r0[cnt] = rC[j]
                cnt = cnt + 1

#       Let operator know we squared filter
#        logger.info('After squaring filter, order: '+str(nn*2))

#       For now and our case, only store causal residues
#       Filters are symmetric and can generate antiCausal residues
        return (pA, zA, gn, p0, r0)

    def _test_N(self):
        """
        Warn the user if the calculated order is too high for a reasonable filter
        design.
        """
        if self.N > 30:
            return qfilter_warning(self, self.N, "Zero-phase Elliptic")
        else:
            return True

#   custom save of filter dictionary

    def _save(self, fil_dict, arg):
        """
        First design initial elliptic filter meeting sqRoot Amp specs;
         - Then create residue vector from poles/zeros;
         - Then square filter (k,p,z and dc,p,r) to get zero phase filter;
         - Then Convert results of filter design to all available formats (pz, pr, ba, sos)
        and store them in the global filter dictionary.

        Corner frequencies and order calculated for minimum filter order are
        also stored to allow for an easy subsequent manual filter optimization.
        """
        fil_save(fil_dict, arg, self.FRMT, __name__)

        # For min. filter order algorithms, update filter dict with calculated
        # new values for filter order N and corner frequency(s) F_PBC

        fil_dict['N'] = self.N
        if str(fil_dict['fo']) == 'min':
            if str(fil_dict['rt']) == 'LP' or str(fil_dict['rt']) == 'HP':
                #               HP or LP - single  corner frequency
                fil_dict['F_PB'] = self.F_PBC / 2.
            else:  # BP or BS - two corner frequencies
                fil_dict['F_PB'] = self.F_PBC[0] / 2.
                fil_dict['F_PB2'] = self.F_PBC[1] / 2.

#       Now generate poles/residues for custom file save of new parameters
        if (not self.manual):
            z = fil_dict['zpk'][0]
            p = fil_dict['zpk'][1]
            k = fil_dict['zpk'][2]
            n = len(z)
            gain, residues = self._partial(k, p, z, n)

            pA, zA, gn, pC, rC = self._sqCausal(k, p, z, gain, residues, n)
            fil_dict['rpk'] = [rC, pC, gn]

            #           save antiCausal b,a (nonReciprocal) also [easier to compute h(n)
            try:
                fil_dict['baA'] = sig.zpk2tf(zA, pA, k)
            except Exception as e:
                logger.error(e)

#       'rpk' is our signal that this is a non-Causal filter with zero phase
#       inserted into fil dictionary after fil_save and convert
# sig_tx -> select_filter -> filter_specs
        self.sig_tx.emit({'sender': __name__, 'filt_changed': 'ellip_zero'})

#------------------------------------------------------------------------------

    def save_filter(self):
        file_filters = ("Text file pole/residue (*.txt_rpk)")
        dlg = QFD(self)
        # return selected file name (with or without extension) and filter (Linux: full text)
        file_name, file_type = dlg.getSaveFileName_(caption="Save filter as",
                                                    directory=dirs.save_dir,
                                                    filter=file_filters)

        file_name = str(file_name)  # QString -> str() needed for Python 2.x
        # Qt5 has QFileDialog.mimeTypeFilters(), but under Qt4 the mime type cannot
        # be extracted reproducibly across file systems, so it is done manually:

        for t in extract_file_ext(
                file_filters):  # get a list of file extensions
            if t in str(file_type):
                file_type = t  # return the last matching extension

        if file_name != "":  # cancelled file operation returns empty string

            # strip extension from returned file name (if any) + append file type:
            file_name = os.path.splitext(file_name)[0] + file_type

            file_type_err = False
            try:

                # save as a custom residue/pole text output for apply with custom tool
                # make sure we have the residues
                if 'rpk' in fb.fil[0]:
                    with io.open(file_name, 'w', encoding="utf8") as f:
                        self.file_dump(f)
                else:
                    file_type_err = True
                    logger.error(
                        'Filter has no residues/poles, cannot save as *.txt_rpk file'
                    )
                if not file_type_err:
                    logger.info('Successfully saved filter as\n\t"{0}"'.format(
                        file_name))
                    dirs.save_dir = os.path.dirname(file_name)  # save new dir

            except IOError as e:
                logger.error('Failed saving "{0}"!\n{1}'.format(file_name, e))

#------------------------------------------------------------------------------

    def file_dump(self, fOut):
        """
        Dump file out in custom text format that apply tool can read to know filter coef's
        """

        #       Fixed format widths for integers and doubles
        intw = '10'
        dblW = 27
        frcW = 20

        #       Fill up character string with filter output
        filtStr = '# IIR filter\n'

        #       parameters that made filter (choose smallest eps)
        #       Amp is stored in Volts (linear units)
        #       the second amp terms aren't really used (for ellip filters)

        FA_PB = fb.fil[0]['A_PB']
        FA_SB = fb.fil[0]['A_SB']
        FAmp = min(FA_PB, FA_SB)

        #       Freq terms in radians so move from -1:1 to -pi:pi
        f_lim = fb.fil[0]['freqSpecsRange']
        f_unit = fb.fil[0]['freq_specs_unit']

        F_S = fb.fil[0]['f_S']
        if fb.fil[0]['freq_specs_unit'] == 'f_S':
            F_S = F_S * 2
        F_SB = fb.fil[0]['F_SB'] * F_S * np.pi
        F_SB2 = fb.fil[0]['F_SB2'] * F_S * np.pi
        F_PB = fb.fil[0]['F_PB'] * F_S * np.pi
        F_PB2 = fb.fil[0]['F_PB2'] * F_S * np.pi

        #       Determine pass/stop bands depending on filter response type
        passMin = []
        passMax = []
        stopMin = []
        stopMax = []

        if fb.fil[0]['rt'] == 'LP':
            passMin = [-F_PB, 0, 0]
            passMax = [F_PB, 0, 0]
            stopMin = [-np.pi, F_SB, 0]
            stopMax = [-F_SB, np.pi, 0]
            f1 = F_PB
            f2 = F_SB
            f3 = f4 = 0
            Ftype = 1
            Fname = 'Low_Pass'

        if fb.fil[0]['rt'] == 'HP':
            passMin = [-np.pi, F_PB, 0]
            passMax = [-F_PB, np.pi, 0]
            stopMin = [-F_SB, 0, 0]
            stopMax = [F_SB, 0, 0]
            f1 = F_SB
            f2 = F_PB
            f3 = f4 = 0
            Ftype = 2
            Fname = 'Hi_Pass'

        if fb.fil[0]['rt'] == 'BS':
            passMin = [-np.pi, -F_PB, F_PB2]
            passMax = [-F_PB2, F_PB, np.pi]
            stopMin = [-F_SB2, F_SB, 0]
            stopMax = [-F_SB, F_SB2, 0]
            f1 = F_PB
            f2 = F_SB
            f3 = F_SB2
            f4 = F_PB2
            Ftype = 4
            Fname = 'Band_Stop'

        if fb.fil[0]['rt'] == 'BP':
            passMin = [-F_PB2, F_PB, 0]
            passMax = [-F_PB, F_PB2, 0]
            stopMin = [-np.pi, -F_SB, F_SB2]
            stopMax = [-F_SB2, F_SB, np.pi]
            f1 = F_SB
            f2 = F_PB
            f3 = F_PB2
            f4 = F_SB2
            Ftype = 3
            Fname = 'Band_Pass'

        filtStr = filtStr + '{:{align}{width}}'.format(
            '10', align='>', width=intw) + ' IIRFILT_4SYM\n'
        filtStr = filtStr + '{:{align}{width}}'.format(
            str(Ftype), align='>', width=intw) + ' ' + Fname + '\n'
        filtStr = filtStr + '{:{d}.{p}f}'.format(FAmp, d=dblW, p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[0], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[0], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[1], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[1], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMin[2], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(passMax[2], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[0], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[0], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[1], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[1], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMin[2], d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(stopMax[2], d=dblW,
                                                  p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(f1, d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(f2, d=dblW, p=frcW) + '\n'
        filtStr = filtStr + '{: {d}.{p}f}'.format(f3, d=dblW, p=frcW)
        filtStr = filtStr + '{: {d}.{p}f}'.format(f4, d=dblW, p=frcW) + '\n'

        #       move pol/res/gain into terms we need
        Fdc = fb.fil[0]['rpk'][2]
        rC = fb.fil[0]['rpk'][0]
        pC = fb.fil[0]['rpk'][1]
        Fnum = len(pC)

        #       Gain term
        filtStr = filtStr + '{: {d}.{p}e}'.format(Fdc, d=dblW, p=frcW) + '\n'

        #       Real pole count inside the unit circle (none of these)

        filtStr = filtStr + '{:{align}{width}}'.format(
            str(0), align='>', width=intw) + '\n'

        #       Complex pole/res count inside the unit circle

        filtStr = filtStr + '{:{i}d}'.format(Fnum, i=intw) + '\n'

        #       Now dump poles/residues
        for j in range(Fnum):
            filtStr = filtStr + '{:{i}d}'.format(j, i=intw) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                rC[j].real, d=dblW, p=frcW) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                rC[j].imag, d=dblW, p=frcW) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                pC[j].real, d=dblW, p=frcW) + ' '
            filtStr = filtStr + '{: {d}.{p}e}'.format(
                pC[j].imag, d=dblW, p=frcW) + '\n'

#       Real pole count outside the unit circle (none of these)
        filtStr = filtStr + '{:{align}{width}}'.format(
            str(0), align='>', width=intw) + '\n'

        #       Complex pole count outside the unit circle (none of these)
        filtStr = filtStr + '{:{align}{width}}'.format(
            str(0), align='>', width=intw) + '\n'

        #       Now write huge text string to file
        fOut.write(filtStr)

#------------------------------------------------------------------------------
#
#         DESIGN ROUTINES
#
#------------------------------------------------------------------------------

# LP: F_PB < F_stop -------------------------------------------------------

    def LPmin(self, fil_dict):
        """Elliptic LP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,
                                      self.F_SB,
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #       force even N
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        #logger.warning("and "+str(self.F_PBC) + " " + str(self.N))
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='low',
                      analog=self.analog,
                      output=self.FRMT))

    def LPman(self, fil_dict):
        """Elliptic LP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PB,
                      btype='low',
                      analog=self.analog,
                      output=self.FRMT))

    # HP: F_stop < F_PB -------------------------------------------------------
    def HPmin(self, fil_dict):
        """Elliptic HP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord(self.F_PB,
                                      self.F_SB,
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #       force even N
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='highpass',
                      analog=self.analog,
                      output=self.FRMT))

    def HPman(self, fil_dict):
        """Elliptic HP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PB,
                      btype='highpass',
                      analog=self.analog,
                      output=self.FRMT))

    # For BP and BS, F_XX have two elements each, A_XX has only one

    # BP: F_SB[0] < F_PB[0], F_SB[1] > F_PB[1] --------------------------------
    def BPmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
                                      [self.F_SB, self.F_SB2],
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #logger.warning(" "+str(self.F_PBC) + " " + str(self.N))
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        #logger.warning("-"+str(self.F_PBC) + " " + str(self.A_SB))
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='bandpass',
                      analog=self.analog,
                      output=self.FRMT))

    def BPman(self, fil_dict):
        """Elliptic BP filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB, [self.F_PB, self.F_PB2],
                      btype='bandpass',
                      analog=self.analog,
                      output=self.FRMT))

    # BS: F_SB[0] > F_PB[0], F_SB[1] < F_PB[1] --------------------------------
    def BSmin(self, fil_dict):
        """Elliptic BP filter, minimum order"""
        self._get_params(fil_dict)
        self.N, self.F_PBC = ellipord([self.F_PB, self.F_PB2],
                                      [self.F_SB, self.F_SB2],
                                      self.A_PB,
                                      self.A_SB,
                                      analog=self.analog)
        #       force even N
        if (self.N % 2) == 1:
            self.N += 1
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB,
                      self.F_PBC,
                      btype='bandstop',
                      analog=self.analog,
                      output=self.FRMT))

    def BSman(self, fil_dict):
        """Elliptic BS filter, manual order"""
        self._get_params(fil_dict)
        if not self._test_N():
            return -1
        self._save(
            fil_dict,
            sig.ellip(self.N,
                      self.A_PB,
                      self.A_SB, [self.F_PB, self.F_PB2],
                      btype='bandstop',
                      analog=self.analog,
                      output=self.FRMT))