@@ -2150,88 +2150,141 @@ def lp2bs(b, a, wo=1.0, bw=1.0):
2150
2150
2151
2151
2152
2152
def bilinear (b , a , fs = 1.0 ):
2153
- r"""
2154
- Return a digital IIR filter from an analog one using a bilinear transform.
2155
-
2156
- Transform a set of poles and zeros from the analog s-plane to the digital
2157
- z-plane using Tustin's method, which substitutes ``2*fs*(z-1) / (z+1)`` for
2158
- ``s``, maintaining the shape of the frequency response.
2153
+ r"""Calculate a digital IIR filter from an analog transfer function by utilizing
2154
+ the bilinear transform.
2159
2155
2160
2156
Parameters
2161
2157
----------
2162
2158
b : array_like
2163
- Numerator of the analog filter transfer function.
2159
+ Coefficients of the numerator polynomial of the analog transfer function in
2160
+ form of a complex- or real-valued 1d array.
2164
2161
a : array_like
2165
- Denominator of the analog filter transfer function.
2162
+ Coefficients of the denominator polynomial of the analog transfer function in
2163
+ form of a complex- or real-valued 1d array.
2166
2164
fs : float
2167
- Sample rate, as ordinary frequency (e.g., hertz). No prewarping is
2165
+ Sample rate, as ordinary frequency (e.g., hertz). No pre-warping is
2168
2166
done in this function.
2169
2167
2170
2168
Returns
2171
2169
-------
2172
- b : ndarray
2173
- Numerator of the transformed digital filter transfer function.
2174
- a : ndarray
2175
- Denominator of the transformed digital filter transfer function.
2170
+ beta : ndarray
2171
+ Coefficients of the numerator polynomial of the digital transfer function in
2172
+ form of a complex- or real-valued 1d array.
2173
+ alpha : ndarray
2174
+ Coefficients of the denominator polynomial of the digital transfer function in
2175
+ form of a complex- or real-valued 1d array.
2176
+
2177
+ Notes
2178
+ -----
2179
+ The parameters :math:`b = [b_0, \ldots, b_Q]` and :math:`a = [a_0, \ldots, a_P]`
2180
+ are 1d arrays of length :math:`Q+1` and :math:`P+1`. They define the analog
2181
+ transfer function
2182
+
2183
+ .. math::
2184
+
2185
+ H_a(s) = \frac{b_0 s^Q + b_1 s^{Q-1} + \cdots + b_Q}{
2186
+ a_0 s^P + a_1 s^{P-1} + \cdots + a_P}\ .
2187
+
2188
+ The bilinear transform [1]_ is applied by substituting
2189
+
2190
+ .. math::
2191
+
2192
+ s = \kappa \frac{z-1}{z+1}\ , \qquad \kappa := 2 f_s\ ,
2193
+
2194
+ into :math:`H_a(s)`, with :math:`f_s` being the sampling rate.
2195
+ This results in the digital transfer function in the :math:`z`-domain
2196
+
2197
+ .. math::
2198
+
2199
+ H_d(z) = \frac{b_0 \left(\kappa \frac{z-1}{z+1}\right)^Q +
2200
+ b_1 \left(\kappa \frac{z-1}{z+1}\right)^{Q-1} +
2201
+ \cdots + b_Q}{
2202
+ a_0 \left(\kappa \frac{z-1}{z+1}\right)^P +
2203
+ a_1 \left(\kappa \frac{z-1}{z+1}\right)^{P-1} +
2204
+ \cdots + a_P}\ .
2205
+
2206
+ This expression can be simplified by multiplying numerator and denominator by
2207
+ :math:`(z+1)^N`, with :math:`N=\max(P, Q)`. This allows :math:`H_d(z)` to be
2208
+ reformulated as
2209
+
2210
+ .. math::
2211
+
2212
+ & & \frac{b_0 \big(\kappa (z-1)\big)^Q (z+1)^{N-Q} +
2213
+ b_1 \big(\kappa (z-1)\big)^{Q-1} (z+1)^{N-Q+1} +
2214
+ \cdots + b_Q(z+1)^N}{
2215
+ a_0 \big(\kappa (z-1)\big)^P (z+1)^{N-P} +
2216
+ a_1 \big(\kappa (z-1)\big)^{P-1} (z+1)^{N-P+1} +
2217
+ \cdots + a_P(z+1)^N}\\
2218
+ &=:& \frac{\beta_0 + \beta_1 z^{-1} + \cdots + \beta_N z^{-N}}{
2219
+ \alpha_0 + \alpha_1 z^{-1} + \cdots + \alpha_N z^{-N}}\ .
2220
+
2221
+
2222
+ This is the equation implemented to perform the bilinear transform. Note that for
2223
+ large :math:`f_s`, :math:`\kappa^Q` or :math:`\kappa^P` can cause a numeric
2224
+ overflow for sufficiently large :math:`P` or :math:`Q`.
2225
+
2226
+ References
2227
+ ----------
2228
+ .. [1] "Bilinear Transform", Wikipedia,
2229
+ https://en.wikipedia.org/wiki/Bilinear_transform
2176
2230
2177
2231
See Also
2178
2232
--------
2179
- lp2lp, lp2hp, lp2bp, lp2bs
2180
- bilinear_zpk
2233
+ lp2lp, lp2hp, lp2bp, lp2bs, bilinear_zpk
2181
2234
2182
2235
Examples
2183
2236
--------
2237
+ The following example shows the frequency response of an analog bandpass filter and
2238
+ the corresponding digital filter derived by utilitzing the bilinear transform:
2239
+
2184
2240
>>> from scipy import signal
2185
2241
>>> import matplotlib.pyplot as plt
2186
2242
>>> import numpy as np
2243
+ ...
2244
+ >>> fs = 100 # sampling frequency
2245
+ >>> om_c = 2 * np.pi * np.array([7, 13]) # corner frequencies
2246
+ >>> bb_s, aa_s = signal.butter(4, om_c, btype='bandpass', analog=True, output='ba')
2247
+ >>> bb_z, aa_z = signal.bilinear(bb_s, aa_s, fs)
2248
+ ...
2249
+ >>> w_z, H_z = signal.freqz(bb_z, aa_z) # frequency response of digitial filter
2250
+ >>> w_s, H_s = signal.freqs(bb_s, aa_s, worN=w_z*fs) # analog filter response
2251
+ ...
2252
+ >>> f_z, f_s = w_z * fs / (2*np.pi), w_s / (2*np.pi)
2253
+ >>> Hz_dB, Hs_dB = (20*np.log10(np.abs(H_).clip(1e-10)) for H_ in (H_z, H_s))
2254
+ >>> fg0, ax0 = plt.subplots()
2255
+ >>> ax0.set_title("Frequency Response of 4-th order Bandpass Filter")
2256
+ >>> ax0.set(xlabel='Frequency $f$ in Hertz', ylabel='Magnitude in dB',
2257
+ ... xlim=[f_z[1], fs/2], ylim=[-200, 2])
2258
+ >>> ax0.semilogx(f_z, Hz_dB, alpha=.5, label=r'$|H_z(e^{j 2 \pi f})|$')
2259
+ >>> ax0.semilogx(f_s, Hs_dB, alpha=.5, label=r'$|H_s(j 2 \pi f)|$')
2260
+ >>> ax0.legend()
2261
+ >>> ax0.grid(which='both', axis='x')
2262
+ >>> ax0.grid(which='major', axis='y')
2263
+ >>> plt.show()
2187
2264
2188
- >>> fs = 100
2189
- >>> bf = 2 * np.pi * np.array([7, 13])
2190
- >>> filts = signal.lti(*signal.butter(4, bf, btype='bandpass',
2191
- ... analog=True))
2192
- >>> filtz = signal.lti(*signal.bilinear(filts.num, filts.den, fs))
2193
- >>> wz, hz = signal.freqz(filtz.num, filtz.den)
2194
- >>> ws, hs = signal.freqs(filts.num, filts.den, worN=fs*wz)
2195
-
2196
- >>> plt.semilogx(wz*fs/(2*np.pi), 20*np.log10(np.abs(hz).clip(1e-15)),
2197
- ... label=r'$|H_z(e^{j \omega})|$')
2198
- >>> plt.semilogx(wz*fs/(2*np.pi), 20*np.log10(np.abs(hs).clip(1e-15)),
2199
- ... label=r'$|H(j \omega)|$')
2200
- >>> plt.legend()
2201
- >>> plt.xlabel('Frequency [Hz]')
2202
- >>> plt.ylabel('Amplitude [dB]')
2203
- >>> plt.grid(True)
2265
+ The difference in the higher frequencies shown in the plot is caused by an effect
2266
+ called "frequency warping". [1]_ describes a method called "pre-warping" to
2267
+ reduce those deviations.
2204
2268
"""
2269
+ b , a = np .atleast_1d (b ), np .atleast_1d (a ) # convert scalars, if needed
2270
+ if not a .ndim == 1 :
2271
+ raise ValueError (f"Parameter a is not a 1d array since { a .shape = } " )
2272
+ if not b .ndim == 1 :
2273
+ raise ValueError (f"Parameter b is not a 1d array since { b .shape = } " )
2274
+ b , a = np .trim_zeros (b , 'f' ), np .trim_zeros (a , 'f' ) # remove leading zeros
2205
2275
fs = _validate_fs (fs , allow_none = False )
2206
- a , b = map (atleast_1d , (a , b ))
2207
- D = len (a ) - 1
2208
- N = len (b ) - 1
2209
- artype = float
2210
- M = max ([N , D ])
2211
- Np = M
2212
- Dp = M
2213
- bprime = np .empty (Np + 1 , artype )
2214
- aprime = np .empty (Dp + 1 , artype )
2215
- for j in range (Np + 1 ):
2216
- val = 0.0
2217
- for i in range (N + 1 ):
2218
- for k in range (i + 1 ):
2219
- for l in range (M - i + 1 ):
2220
- if k + l == j :
2221
- val += (comb (i , k ) * comb (M - i , l ) * b [N - i ] *
2222
- pow (2 * fs , i ) * (- 1 ) ** k )
2223
- bprime [j ] = real (val )
2224
- for j in range (Dp + 1 ):
2225
- val = 0.0
2226
- for i in range (D + 1 ):
2227
- for k in range (i + 1 ):
2228
- for l in range (M - i + 1 ):
2229
- if k + l == j :
2230
- val += (comb (i , k ) * comb (M - i , l ) * a [D - i ] *
2231
- pow (2 * fs , i ) * (- 1 ) ** k )
2232
- aprime [j ] = real (val )
2233
2276
2234
- return normalize (bprime , aprime )
2277
+ # Splitting the factor fs*2 between numerator and denominator reduces the chance of
2278
+ # numeric overflow for large fs and large N:
2279
+ fac = np .sqrt (fs * 2 )
2280
+ zp1 = np .polynomial .Polynomial ((+ 1 , 1 )) / fac # Polynomial (z + 1) / fac
2281
+ zm1 = np .polynomial .Polynomial ((- 1 , 1 )) * fac # Polynomial (z - 1) * fac
2282
+ # Note that NumPy's Polynomial coefficient order is backward compared to a and b.
2283
+
2284
+ N = max (len (a ), len (b )) - 1
2285
+ numerator = sum (b_ * zp1 ** (N - q ) * zm1 ** q for q , b_ in enumerate (b [::- 1 ]))
2286
+ denominator = sum (a_ * zp1 ** (N - p ) * zm1 ** p for p , a_ in enumerate (a [::- 1 ]))
2287
+ return normalize (numerator .coef [::- 1 ], denominator .coef [::- 1 ])
2235
2288
2236
2289
2237
2290
def _validate_gpass_gstop (gpass , gstop ):
0 commit comments