24
24
25
25
import warnings
26
26
27
- from ansys .dpf .core import Field , Operator
27
+ from ansys .dpf .core import Field , Operator , TimeFreqSupport , fields_factory , locations
28
28
import matplotlib .pyplot as plt
29
29
import numpy as np
30
+ import scipy
30
31
31
32
from .._pyansys_sound import PyAnsysSoundException , PyAnsysSoundWarning
32
33
from ..signal_processing import SignalProcessingParent
@@ -42,10 +43,14 @@ class Filter(SignalProcessingParent):
42
43
This class allows designing, loading, and applying a digital filter to a signal. The filter
43
44
coefficients can be provided directly, using the attributes :attr:`b_coefficients` and
44
45
:attr:`a_coefficients`, or computed from a specific frequency response function (FRF), using
45
- the methods :meth:`design_FIR_from_FRF ` or :meth:`design_FIR_from_FRF_file`. In this latter
46
- case, the filter is designed as a minimum-phase FIR filter, and the filter denominator
46
+ the attribute :attr:`frf ` or the method :meth:`design_FIR_from_FRF_file`. In this latter case,
47
+ the filter is designed as a minimum-phase FIR filter, and the filter denominator
47
48
(:attr:`a_coefficients`) is set to 1 as a consequence.
48
49
50
+ Note that only one filter definition source (coefficients, FRF, or FRF file) can be provided
51
+ when instantiating the class. After class instantiation, anytime the coefficients are changed,
52
+ the FRF is updated accordingly, and vice versa.
53
+
49
54
Filtering a signal consists in applying the filter coefficients :math:`b[k]` and :math:`a[k]`
50
55
in the following difference equation, with :math:`x[n]` the input signal, and :math:`y[n]` the
51
56
output signal:
@@ -66,6 +71,7 @@ def __init__(
66
71
b_coefficients : list [float ] = None ,
67
72
a_coefficients : list [float ] = None ,
68
73
sampling_frequency : float = 44100.0 ,
74
+ frf : Field = None ,
69
75
file : str = "" ,
70
76
signal : Field = None ,
71
77
):
@@ -74,16 +80,21 @@ def __init__(
74
80
Parameters
75
81
----------
76
82
a_coefficients : list[float], default: None
77
- Denominator coefficients of the filter.
83
+ Denominator coefficients of the filter. This is mutually exclusive with parameters
84
+ ``frf`` and ``file``.
78
85
b_coefficients : list[float], default: None
79
- Numerator coefficients of the filter.
86
+ Numerator coefficients of the filter. This is mutually exclusive with parameters ``frf``
87
+ and ``file``.
80
88
sampling_frequency : float, default: 44100.0
81
89
Sampling frequency associated with the filter coefficients, in Hz.
90
+ frf : Field, default: None
91
+ Frequency response function (FRF) of the filter, in dB. This is mutually exclusive with
92
+ parameters ``a_coefficients``, ``b_coefficients``, and ``file``.
82
93
file : str, default: ""
83
94
Path to the file containing the frequency response function (FRF) to load. The text
84
95
file shall have the same text format (with the header `AnsysSound_FRF`), as supported
85
- by Ansys Sound SAS. If ``file`` is specified, parameters ``a_coefficients`` and
86
- ``b_coefficients`` are ignored .
96
+ by Ansys Sound SAS. This is mutually exclusive with parameters ``a_coefficients``,
97
+ ``b_coefficients``, and ``frf`` .
87
98
signal : Field, default: None
88
99
Signal to filter.
89
100
"""
@@ -95,18 +106,28 @@ def __init__(
95
106
96
107
self .__sampling_frequency = sampling_frequency
97
108
98
- if file != "" :
99
- if a_coefficients is not None or b_coefficients is not None :
100
- warnings .warn (
101
- PyAnsysSoundWarning (
102
- "Specified parameters a_coefficients and b_coefficients are ignored "
103
- "because FRF file is also specified."
104
- )
105
- )
106
- self .design_FIR_from_FRF_file (file )
107
- else :
109
+ # Initialize attributes before processing arguments (because of their mutual dependencies).
110
+ self .__a_coefficients = None
111
+ self .__b_coefficients = None
112
+ self .__frf = None
113
+
114
+ # Check which filter definition source (coefficients, FRF, or FRF file) is provided (there
115
+ # should be less than 2).
116
+ is_coefficients_specified = not (a_coefficients is None and b_coefficients is None )
117
+ is_frf_specified = frf is not None
118
+ is_frf_file_specified = file != ""
119
+ if (is_coefficients_specified + is_frf_specified + is_frf_file_specified ) > 1 :
120
+ raise PyAnsysSoundException (
121
+ "Only one filter definition source (coefficients, FRF, or FRF file) must be "
122
+ "provided. Specify either `a_coefficients` and `b_coefficients`, `frf`, or `file`."
123
+ )
124
+ elif a_coefficients is not None or b_coefficients is not None :
108
125
self .a_coefficients = a_coefficients
109
126
self .b_coefficients = b_coefficients
127
+ elif frf is not None :
128
+ self .frf = frf
129
+ elif file != "" :
130
+ self .design_FIR_from_FRF_file (file )
110
131
111
132
self .signal = signal
112
133
@@ -142,6 +163,9 @@ def a_coefficients(self, coefficients: list[float]):
142
163
"""Set filter's denominator coefficients."""
143
164
self .__a_coefficients = coefficients
144
165
166
+ # Update the FRF to match the new coefficients (if both are set).
167
+ self .__compute_FRF_from_coefficients ()
168
+
145
169
@property
146
170
def b_coefficients (self ) -> list [float ]:
147
171
"""Numerator coefficients of the filter's transfer function."""
@@ -152,6 +176,34 @@ def b_coefficients(self, coefficients: list[float]):
152
176
"""Set filter's numerator coefficients."""
153
177
self .__b_coefficients = coefficients
154
178
179
+ # Update the FRF to match the new coefficients (if both are set).
180
+ self .__compute_FRF_from_coefficients ()
181
+
182
+ @property
183
+ def frf (self ) -> Field :
184
+ """Frequency response function (FRF) of the filter.
185
+
186
+ Contains the response magnitude in dB of the filter as a function of frequency.
187
+ """
188
+ return self .__frf
189
+
190
+ @frf .setter
191
+ def frf (self , frf : Field ):
192
+ """Set frequency response function."""
193
+ if frf is not None :
194
+ if not (isinstance (frf , Field )):
195
+ raise PyAnsysSoundException ("Specified FRF must be provided as a DPF field." )
196
+
197
+ freq_data = frf .time_freq_support .time_frequencies .data
198
+ if len (frf .data ) < 2 or len (freq_data ) < 2 :
199
+ raise PyAnsysSoundException (
200
+ "Specified FRF must have at least two frequency points."
201
+ )
202
+ self .__frf = frf
203
+
204
+ # Update coefficients to match the FRF.
205
+ self .__compute_coefficients_from_FRF ()
206
+
155
207
@property
156
208
def signal (self ) -> Field :
157
209
"""Input signal."""
@@ -213,61 +265,30 @@ def design_FIR_from_FRF_file(self, file: str):
213
265
self .__operator_load .run ()
214
266
215
267
# Get the output.
216
- frf = self .__operator_load .get_output (0 , "field" )
217
-
218
- # Compute the filter coefficients.
219
- self .design_FIR_from_FRF (frf )
220
-
221
- def design_FIR_from_FRF (self , frf : Field ):
222
- """Design a minimum-phase FIR filter from a frequency response function (FRF).
223
-
224
- Computes the filter coefficients according to the filter sampling frequency and the
225
- provided FRF data.
226
-
227
- .. note::
228
- If the maximum frequency specified in the FRF extends beyond half the filter sampling
229
- frequency, the FRF data is truncated to this frequency. If, on the contrary, the FRF
230
- maximum frequency is lower than half the filter sampling frequency, the FRF is
231
- zero-padded between the two.
232
-
233
- Parameters
234
- ----------
235
- frf : Field
236
- Frequency response function (FRF).
237
- """
238
- # Set operator inputs.
239
- self .__operator_design .connect (0 , frf )
240
- self .__operator_design .connect (1 , self .__sampling_frequency )
241
-
242
- # Run the operator.
243
- self .__operator_design .run ()
244
-
245
- # Get the output.
246
- self .b_coefficients = list (map (float , self .__operator_design .get_output (0 , "vec_double" )))
247
- self .a_coefficients = list (map (float , self .__operator_design .get_output (1 , "vec_double" )))
268
+ self .frf = self .__operator_load .get_output (0 , "field" )
248
269
249
270
def process (self ):
250
271
"""Filter the signal with the current coefficients."""
251
272
# Check input signal.
252
273
if self .signal is None :
253
274
raise PyAnsysSoundException (
254
- f"Input signal is not set. Use { __class__ .__name__ } .signal."
275
+ f"Input signal is not set. Use ` { __class__ .__name__ } .signal` ."
255
276
)
256
277
257
278
if self .a_coefficients is None or len (self .a_coefficients ) == 0 :
258
279
raise PyAnsysSoundException (
259
280
"Filter's denominator coefficients (a_coefficients) must be defined and cannot be "
260
- f"empty. Use { __class__ .__name__ } .a_coefficients, or the methods "
261
- f"{ __class__ .__name__ } .design_FIR_from_FRF() or "
262
- f"{ __class__ .__name__ } .design_FIR_from_FRF_file()."
281
+ f"empty. Use ` { __class__ .__name__ } .a_coefficients`, "
282
+ f"` { __class__ .__name__ } .frf`, or the "
283
+ f"` { __class__ .__name__ } .design_FIR_from_FRF_file()` method ."
263
284
)
264
285
265
286
if self .b_coefficients is None or len (self .b_coefficients ) == 0 :
266
287
raise PyAnsysSoundException (
267
288
"Filter's numerator coefficients (b_coefficients) must be defined and cannot be "
268
- f"empty. Use { __class__ .__name__ } .b_coefficients, or the methods "
269
- f"{ __class__ .__name__ } .design_FIR_from_FRF() or "
270
- f"{ __class__ .__name__ } .design_FIR_from_FRF_file()."
289
+ f"empty. Use ` { __class__ .__name__ } .b_coefficients`, "
290
+ f"` { __class__ .__name__ } .frf`, or the "
291
+ f"` { __class__ .__name__ } .design_FIR_from_FRF_file()` method ."
271
292
)
272
293
273
294
# Set operator inputs.
@@ -293,7 +314,7 @@ def get_output(self) -> Field:
293
314
warnings .warn (
294
315
PyAnsysSoundWarning (
295
316
"Output is not processed yet. "
296
- f"Use the { __class__ .__name__ } .process() method."
317
+ f"Use the ` { __class__ .__name__ } .process()` method."
297
318
)
298
319
)
299
320
return self ._output
@@ -317,7 +338,7 @@ def plot(self):
317
338
"""Plot the filtered signal in a figure."""
318
339
if self ._output == None :
319
340
raise PyAnsysSoundException (
320
- f"Output is not processed yet. Use the { __class__ .__name__ } .process() method."
341
+ f"Output is not processed yet. Use the ` { __class__ .__name__ } .process()` method."
321
342
)
322
343
output = self .get_output ()
323
344
@@ -329,3 +350,93 @@ def plot(self):
329
350
plt .ylabel ("Amplitude" )
330
351
plt .grid (True )
331
352
plt .show ()
353
+
354
+ def plot_FRF (self ):
355
+ """Plot the frequency response function (FRF) of the filter."""
356
+ if self .frf is None :
357
+ raise PyAnsysSoundException (
358
+ "Filter's frequency response function (FRF) is not set. Use "
359
+ f"`{ __class__ .__name__ } .frf`, or `{ __class__ .__name__ } .a_coefficients` and "
360
+ f"`{ __class__ .__name__ } .b_coefficients`, or the "
361
+ f"`{ __class__ .__name__ } .design_FIR_from_FRF_file()` method."
362
+ )
363
+
364
+ plt .plot (self .frf .time_freq_support .time_frequencies .data , self .frf .data )
365
+ plt .title ("Frequency response function (FRF) of the filter" )
366
+ plt .xlabel ("Frequency (Hz)" )
367
+ plt .ylabel ("Magnitude (dB)" )
368
+ plt .grid (True )
369
+ plt .show ()
370
+
371
+ def __compute_coefficients_from_FRF (self ):
372
+ """Design a minimum-phase FIR filter from the frequency response function (FRF).
373
+
374
+ Computes the filter coefficients according to the filter sampling frequency and the
375
+ currently set FRF.
376
+
377
+ .. note::
378
+ If the maximum frequency in the FRF extends beyond half the filter sampling frequency,
379
+ the FRF data is truncated to this frequency to compute the coefficients. If, on the
380
+ contrary, the FRF maximum frequency is lower than half the filter sampling frequency,
381
+ the FRF data is zero-padded between the two.
382
+ """
383
+ if self .frf is None :
384
+ self .__a_coefficients = None
385
+ self .__b_coefficients = None
386
+ else :
387
+ self .__operator_design .connect (0 , self .frf )
388
+ self .__operator_design .connect (1 , self .__sampling_frequency )
389
+
390
+ self .__operator_design .run ()
391
+
392
+ # Bypass the coefficients setters to avoid infinite loops.
393
+ self .__b_coefficients = list (
394
+ map (float , self .__operator_design .get_output (0 , "vec_double" ))
395
+ )
396
+ self .__a_coefficients = list (
397
+ map (float , self .__operator_design .get_output (1 , "vec_double" ))
398
+ )
399
+
400
+ def __compute_FRF_from_coefficients (self ):
401
+ """Compute the frequency response function (FRF) from the filter coefficients.
402
+
403
+ Computes the FRF from the filter coefficients, using the function ``scipy.signal.freqz()``.
404
+ If either the numerator or denominator coefficients are empty or not set, the FRF is set to
405
+ ``None``.
406
+
407
+ .. note::
408
+ The computed FRF length is equal to the number of coefficients in the filter's
409
+ numerator.
410
+ """
411
+ if (
412
+ self .b_coefficients is None
413
+ or self .a_coefficients is None
414
+ or len (self .b_coefficients ) == 0
415
+ or len (self .a_coefficients ) == 0
416
+ ):
417
+ self .__frf = None
418
+ else :
419
+ freq , complex_response = scipy .signal .freqz (
420
+ b = self .b_coefficients ,
421
+ a = self .a_coefficients ,
422
+ worN = len (self .b_coefficients ),
423
+ whole = False ,
424
+ plot = None ,
425
+ fs = self .__sampling_frequency ,
426
+ include_nyquist = True ,
427
+ )
428
+
429
+ f_freq = fields_factory .create_scalar_field (
430
+ num_entities = 1 , location = locations .time_freq
431
+ )
432
+ f_freq .append (freq , 1 )
433
+
434
+ frf_support = TimeFreqSupport ()
435
+ frf_support .time_frequencies = f_freq
436
+
437
+ # Bypass the FRF setter to avoid infinite loops.
438
+ self .__frf = fields_factory .create_scalar_field (
439
+ num_entities = 1 , location = locations .time_freq
440
+ )
441
+ self .__frf .append (20 * np .log10 (abs (complex_response )), 1 )
442
+ self .__frf .time_freq_support = frf_support
0 commit comments