3
3
from math import ceil , log
4
4
import operator
5
5
import warnings
6
+ from typing import Literal , Optional
6
7
7
8
import numpy as np
8
9
from numpy .fft import irfft , fft , ifft
@@ -1022,8 +1023,10 @@ def firls(numtaps, bands, desired, *, weight=None, nyq=_NoValue, fs=None):
1022
1023
# check remaining params
1023
1024
desired = np .asarray (desired ).flatten ()
1024
1025
if bands .size != desired .size :
1025
- raise ValueError ("desired must have one entry per frequency, got {} "
1026
- "gains for {} frequencies." .format (desired .size , bands .size ))
1026
+ raise ValueError (
1027
+ f"desired must have one entry per frequency, got { desired .size } "
1028
+ f"gains for { bands .size } frequencies."
1029
+ )
1027
1030
desired .shape = (- 1 , 2 )
1028
1031
if (np .diff (bands ) <= 0 ).any () or (np .diff (bands [:, 0 ]) < 0 ).any ():
1029
1032
raise ValueError ("bands must be monotonically nondecreasing and have "
@@ -1125,21 +1128,26 @@ def _dhtm(mag):
1125
1128
return recon
1126
1129
1127
1130
1128
- def minimum_phase (h , method = 'homomorphic' , n_fft = None ):
1131
+ def minimum_phase (h : np .ndarray ,
1132
+ method : Literal ['homomorphic' , 'hilbert' ] = 'homomorphic' ,
1133
+ n_fft : Optional [int ] = None , * , half : bool = True ) -> np .ndarray :
1129
1134
"""Convert a linear-phase FIR filter to minimum phase
1130
1135
1131
1136
Parameters
1132
1137
----------
1133
1138
h : array
1134
1139
Linear-phase FIR filter coefficients.
1135
1140
method : {'hilbert', 'homomorphic'}
1136
- The method to use :
1141
+ The provided methods are :
1137
1142
1138
1143
'homomorphic' (default)
1139
1144
This method [4]_ [5]_ works best with filters with an
1140
1145
odd number of taps, and the resulting minimum phase filter
1141
1146
will have a magnitude response that approximates the square
1142
- root of the original filter's magnitude response.
1147
+ root of the original filter's magnitude response using half
1148
+ the number of taps when ``half=True`` (default), or the
1149
+ original magnitude spectrum using the same number of taps
1150
+ when ``half=False``.
1143
1151
1144
1152
'hilbert'
1145
1153
This method [1]_ is designed to be used with equiripple
@@ -1149,12 +1157,20 @@ def minimum_phase(h, method='homomorphic', n_fft=None):
1149
1157
n_fft : int
1150
1158
The number of points to use for the FFT. Should be at least a
1151
1159
few times larger than the signal length (see Notes).
1160
+ half : bool
1161
+ If ``True``, create a filter that is half the length of the original, with a
1162
+ magnitude spectrum that is the square root of the original. If ``False``,
1163
+ create a filter that is the same length as the original, with a magnitude
1164
+ spectrum that is designed to match the original (only supported when
1165
+ ``method='homomorphic'``).
1166
+
1167
+ .. versionadded:: 1.14.0
1152
1168
1153
1169
Returns
1154
1170
-------
1155
1171
h_minimum : array
1156
1172
The minimum-phase version of the filter, with length
1157
- ``(length (h) + 1) // 2``.
1173
+ ``(len (h) + 1) // 2`` when ``half is True`` or ``len(h)`` otherwise .
1158
1174
1159
1175
See Also
1160
1176
--------
@@ -1185,9 +1201,8 @@ def minimum_phase(h, method='homomorphic', n_fft=None):
1185
1201
1186
1202
Alternative implementations exist for creating minimum-phase filters,
1187
1203
including zero inversion [2]_ and spectral factorization [3]_ [4]_.
1188
- For more information, see:
1189
-
1190
- http://dspguru.com/dsp/howtos/how-to-design-minimum-phase-fir-filters
1204
+ For more information, see `this DSPGuru page
1205
+ <http://dspguru.com/dsp/howtos/how-to-design-minimum-phase-fir-filters>`__.
1191
1206
1192
1207
References
1193
1208
----------
@@ -1205,49 +1220,66 @@ def minimum_phase(h, method='homomorphic', n_fft=None):
1205
1220
.. [4] J. S. Lim, Advanced Topics in Signal Processing.
1206
1221
Englewood Cliffs, N.J.: Prentice Hall, 1988.
1207
1222
.. [5] A. V. Oppenheim, R. W. Schafer, and J. R. Buck,
1208
- "Discrete-Time Signal Processing," 2nd edition.
1209
- Upper Saddle River, N.J.: Prentice Hall, 1999 .
1223
+ "Discrete-Time Signal Processing," 3rd edition.
1224
+ Upper Saddle River, N.J.: Pearson, 2009 .
1210
1225
1211
1226
Examples
1212
1227
--------
1213
- Create an optimal linear-phase filter, then convert it to minimum phase:
1228
+ Create an optimal linear-phase low-pass filter `h` with a transition band of
1229
+ [0.2, 0.3] (assuming a Nyquist frequency of 1):
1214
1230
1215
1231
>>> import numpy as np
1216
1232
>>> from scipy.signal import remez, minimum_phase, freqz, group_delay
1217
1233
>>> import matplotlib.pyplot as plt
1218
1234
>>> freq = [0, 0.2, 0.3, 1.0]
1219
1235
>>> desired = [1, 0]
1220
- >>> h_linear = remez(151, freq, desired, fs=2. )
1236
+ >>> h_linear = remez(151, freq, desired, fs=2)
1221
1237
1222
1238
Convert it to minimum phase:
1223
1239
1224
- >>> h_min_hom = minimum_phase(h_linear, method='homomorphic')
1225
- >>> h_min_hil = minimum_phase(h_linear, method='hilbert')
1226
-
1227
- Compare the three filters:
1228
-
1229
- >>> fig, axs = plt.subplots(4, figsize=(4, 8))
1230
- >>> for h, style, color in zip((h_linear, h_min_hom, h_min_hil),
1231
- ... ('-', '-', '--'), ('k', 'r', 'c')):
1232
- ... w, H = freqz(h)
1233
- ... w, gd = group_delay((h, 1))
1234
- ... w /= np.pi
1235
- ... axs[0].plot(h, color=color, linestyle=style)
1236
- ... axs[1].plot(w, np.abs(H), color=color, linestyle=style)
1237
- ... axs[2].plot(w, 20 * np.log10(np.abs(H)), color=color, linestyle=style)
1238
- ... axs[3].plot(w, gd, color=color, linestyle=style)
1239
- >>> for ax in axs:
1240
- ... ax.grid(True, color='0.5')
1241
- ... ax.fill_between(freq[1:3], *ax.get_ylim(), color='#ffeeaa', zorder=1)
1242
- >>> axs[0].set(xlim=[0, len(h_linear) - 1], ylabel='Amplitude', xlabel='Samples')
1243
- >>> axs[1].legend(['Linear', 'Min-Hom', 'Min-Hil'], title='Phase')
1244
- >>> for ax, ylim in zip(axs[1:], ([0, 1.1], [-150, 10], [-60, 60])):
1245
- ... ax.set(xlim=[0, 1], ylim=ylim, xlabel='Frequency')
1246
- >>> axs[1].set(ylabel='Magnitude')
1247
- >>> axs[2].set(ylabel='Magnitude (dB)')
1248
- >>> axs[3].set(ylabel='Group delay')
1249
- >>> plt.tight_layout()
1240
+ >>> h_hil = minimum_phase(h_linear, method='hilbert')
1241
+ >>> h_hom = minimum_phase(h_linear, method='homomorphic')
1242
+ >>> h_hom_full = minimum_phase(h_linear, method='homomorphic', half=False)
1243
+
1244
+ Compare the impulse and frequency response of the four filters:
1245
+
1246
+ >>> fig0, ax0 = plt.subplots(figsize=(6, 3), tight_layout=True)
1247
+ >>> fig1, axs = plt.subplots(3, sharex='all', figsize=(6, 6), tight_layout=True)
1248
+ >>> ax0.set_title("Impulse response")
1249
+ >>> ax0.set(xlabel='Samples', ylabel='Amplitude', xlim=(0, len(h_linear) - 1))
1250
+ >>> axs[0].set_title("Frequency Response")
1251
+ >>> axs[0].set(xlim=(0, .65), ylabel="Magnitude / dB")
1252
+ >>> axs[1].set(ylabel="Phase / rad")
1253
+ >>> axs[2].set(ylabel="Group Delay / samples", ylim=(-31, 81),
1254
+ ... xlabel='Normalized Frequency (Nyqist frequency: 1)')
1255
+ >>> for h, lb in ((h_linear, f'Linear ({len(h_linear)})'),
1256
+ ... (h_hil, f'Min-Hilbert ({len(h_hil)})'),
1257
+ ... (h_hom, f'Min-Homomorphic ({len(h_hom)})'),
1258
+ ... (h_hom_full, f'Min-Homom. Full ({len(h_hom_full)})')):
1259
+ ... w_H, H = freqz(h, fs=2)
1260
+ ... w_gd, gd = group_delay((h, 1), fs=2)
1261
+ ...
1262
+ ... alpha = 1.0 if lb == 'linear' else 0.5 # full opacity for 'linear' line
1263
+ ... ax0.plot(h, '.-', alpha=alpha, label=lb)
1264
+ ... axs[0].plot(w_H, 20 * np.log10(np.abs(H)), alpha=alpha)
1265
+ ... axs[1].plot(w_H, np.unwrap(np.angle(H)), alpha=alpha, label=lb)
1266
+ ... axs[2].plot(w_gd, gd, alpha=alpha)
1267
+ >>> ax0.grid(True)
1268
+ >>> ax0.legend(title='Filter Phase (Order)')
1269
+ >>> axs[1].legend(title='Filter Phase (Order)', loc='lower right')
1270
+ >>> for ax_ in axs: # shade transition band:
1271
+ ... ax_.axvspan(freq[1], freq[2], color='y', alpha=.25)
1272
+ ... ax_.grid(True)
1273
+ >>> plt.show()
1250
1274
1275
+ The impulse response and group delay plot depict the 75 sample delay of the linear
1276
+ phase filter `h`. The phase should also be linear in the stop band--due to the small
1277
+ magnitude, numeric noise dominates there. Furthermore, the plots show that the
1278
+ minimum phase filters clearly show a reduced (negative) phase slope in the pass and
1279
+ transition band. The plots also illustrate that the filter with parameters
1280
+ ``method='homomorphic', half=False`` has same order and magnitude response as the
1281
+ linear filter `h` wheras the other minimum phase filters have only half the order
1282
+ and the square root of the magnitude response.
1251
1283
"""
1252
1284
h = np .asarray (h )
1253
1285
if np .iscomplexobj (h ):
@@ -1261,6 +1293,8 @@ def minimum_phase(h, method='homomorphic', n_fft=None):
1261
1293
if not isinstance (method , str ) or method not in \
1262
1294
('homomorphic' , 'hilbert' ,):
1263
1295
raise ValueError (f'method must be "homomorphic" or "hilbert", got { method !r} ' )
1296
+ if method == "hilbert" and not half :
1297
+ raise ValueError ("`half=False` is only supported when `method='homomorphic'`" )
1264
1298
if n_fft is None :
1265
1299
n_fft = 2 ** int (np .ceil (np .log2 (2 * (len (h ) - 1 ) / 0.01 )))
1266
1300
n_fft = int (n_fft )
@@ -1283,19 +1317,22 @@ def minimum_phase(h, method='homomorphic', n_fft=None):
1283
1317
# take 0.25*log(|H|**2) = 0.5*log(|H|)
1284
1318
h_temp += 1e-7 * h_temp [h_temp > 0 ].min () # don't let log blow up
1285
1319
np .log (h_temp , out = h_temp )
1286
- h_temp *= 0.5
1320
+ if half : # halving of magnitude spectrum optional
1321
+ h_temp *= 0.5
1287
1322
# IDFT
1288
1323
h_temp = ifft (h_temp ).real
1289
1324
# multiply pointwise by the homomorphic filter
1290
1325
# lmin[n] = 2u[n] - d[n]
1326
+ # i.e., double the positive frequencies and zero out the negative ones;
1327
+ # Oppenheim+Shafer 3rd ed p991 eq13.42b and p1004 fig13.7
1291
1328
win = np .zeros (n_fft )
1292
1329
win [0 ] = 1
1293
- stop = ( len ( h ) + 1 ) // 2
1330
+ stop = n_fft // 2
1294
1331
win [1 :stop ] = 2
1295
- if len ( h ) % 2 :
1332
+ if n_fft % 2 :
1296
1333
win [stop ] = 1
1297
1334
h_temp *= win
1298
1335
h_temp = ifft (np .exp (fft (h_temp )))
1299
1336
h_minimum = h_temp .real
1300
- n_out = n_half + len (h ) % 2
1337
+ n_out = ( n_half + len (h ) % 2 ) if half else len ( h )
1301
1338
return h_minimum [:n_out ]
0 commit comments