Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Harmonize frequency range parameter #589

Merged
merged 10 commits into from Apr 26, 2024
4 changes: 2 additions & 2 deletions docs/concepts/resources/fft_norms.py
Expand Up @@ -11,9 +11,9 @@
impulse = pf.signals.impulse(
n_samples, sampling_rate=sampling_rate)
filter = pf.dsp.filter.fractional_octave_bands(
impulse, num_fractions=1, freq_range=(500, 700))
impulse, num_fractions=1, frequency_range=(500, 700))
ir = pf.dsp.filter.fractional_octave_bands(
impulse, num_fractions=1, freq_range=(200, 400), order=3)*n_samples/2
impulse, num_fractions=1, frequency_range=(200, 400), order=3)*n_samples/2
sine = pf.signals.sine(1e3, n_samples, sampling_rate=sampling_rate)
noise = pf.signals.noise(
n_samples, rms=1/np.sqrt(2), sampling_rate=sampling_rate)
Expand Down
3 changes: 2 additions & 1 deletion docs/concepts/resources/filter_types.py
Expand Up @@ -118,7 +118,8 @@

# DIN Filterbank
axis = ax[0]
y = pf.dsp.filter.fractional_octave_bands(impulse, 1, freq_range=(60, 12e3))
y = pf.dsp.filter.fractional_octave_bands(impulse, 1,
frequency_range=(60, 12e3))
pf.plot.freq(y, ax=axis)
axis.set_title('Fractional octave bands (room acoustics)')
axis.set_xlim(20, 20e3)
Expand Down
50 changes: 50 additions & 0 deletions pyfar/_utils.py
@@ -0,0 +1,50 @@
from pyfar.classes.warnings import PyfarDeprecationWarning
import warnings


# Decorator function to rename parameters to be deprecated
def rename_arg(arg_map, warning_message):
"""
Function for deprecating or renaming arguments.
hoyer-a marked this conversation as resolved.
Show resolved Hide resolved

Intercepts input if a deprecated argument is passed, replaces it with
a new argument and raises a PyfarDeprecationWarning.

Parameters
-----------
arg_map : dictionary
Map with deprecated argument to intercept and new argument to
replace it with: ``{"deprecated_argument": "new_argument"}``
warning_message: string
Message to be thrown with deprecation warning.

Returns
---------
function : function
Modified function with replaced arguments.

Examples
---------
Following example shows how a deprecated argument can be replaced by a
new argument while throwing a deprecation warning:

>>> from pyfar._utils import rename_arg
>>>
>>> @rename_arg({"old_arg": "new_arg"}, "old-arg will be deprecated in"
>>> " version x.x.x in favor of new_arg")
>>> def function(arg1, arg2, new_arg):
>>> return arg1, arg2, new_arg
>>>
>>> function(1, 2, old_arg=3)
"""

def decorator(func):
def wrapper(*args, **kwargs):
new_kwargs = {}
for kwarg, value in kwargs.items():
if kwarg in arg_map:
warnings.warn((warning_message), PyfarDeprecationWarning)
new_kwargs[arg_map.get(kwarg, kwarg)] = value
return func(*args, **new_kwargs)
return wrapper
return decorator
50 changes: 35 additions & 15 deletions pyfar/dsp/dsp.py
Expand Up @@ -4,6 +4,7 @@
import pyfar
from pyfar.dsp import fft
from pyfar.classes.warnings import PyfarDeprecationWarning
from pyfar._utils import rename_arg
import warnings


Expand Down Expand Up @@ -717,8 +718,11 @@ def _time_window_symmetric_interval_four(interval, window):
return win, win_start, win_stop


@rename_arg({"freq_range": "frequency_range"},
"freq_range parameter will be deprecated in pyfar 0.8.0 in "
"favor of frequency_range")
def regularized_spectrum_inversion(
signal, freq_range,
signal, frequency_range,
regu_outside=1., regu_inside=10**(-200/20), regu_final=None,
normalized=True):
r"""Invert the spectrum of a signal applying frequency dependent
Expand Down Expand Up @@ -747,7 +751,7 @@ def regularized_spectrum_inversion(
----------
signal : Signal
The signals which spectra are to be inverted.
freq_range : tuple, array_like, double
frequency_range : tuple, array_like, double
The upper and lower frequency limits outside of which the
regularization factor is to be applied.
regu_outside : float, optional
Expand All @@ -763,6 +767,11 @@ def regularized_spectrum_inversion(
normalized : bool
Flag to indicate if the normalized spectrum (according to
`signal.fft_norm`) should be inverted. The default is ``True``.
freq_range: tuple, array_like, double
hoyer-a marked this conversation as resolved.
Show resolved Hide resolved
The upper and lower frequency limits outside of which the
regularization factor is to be applied.
``'freq_range'`` parameter will be deprecated in pyfar 0.8.0 in favor
of ``'frequency_range'``.


Returns
Expand Down Expand Up @@ -791,9 +800,9 @@ def regularized_spectrum_inversion(
else:
data = signal.freq_raw

freq_range = np.asarray(freq_range)
frequency_range = np.asarray(frequency_range)

if freq_range.size < 2:
if frequency_range.size < 2:
raise ValueError(
"The frequency range needs to specify lower and upper limits.")

Expand All @@ -802,14 +811,15 @@ def regularized_spectrum_inversion(
regu_outside = np.ones(signal.n_bins, dtype=np.double) * regu_outside

idx_xfade_lower = signal.find_nearest_frequency(
[freq_range[0]/np.sqrt(2), freq_range[0]])
[frequency_range[0]/np.sqrt(2), frequency_range[0]])

regu_final = _cross_fade(regu_outside, regu_inside, idx_xfade_lower)

if freq_range[1] < signal.sampling_rate/2:
if frequency_range[1] < signal.sampling_rate/2:
idx_xfade_upper = signal.find_nearest_frequency([
freq_range[1],
np.min([freq_range[1]*np.sqrt(2), signal.sampling_rate/2])])
frequency_range[1],
np.min([frequency_range[1]*np.sqrt(2),
signal.sampling_rate/2])])

regu_final = _cross_fade(regu_final, regu_outside, idx_xfade_upper)

Expand Down Expand Up @@ -1418,8 +1428,11 @@ def find_impulse_response_start(
return np.squeeze(start_sample)


def deconvolve(system_output, system_input, fft_length=None, freq_range=None,
**kwargs):
@rename_arg({"freq_range": "frequency_range"},
"freq_range parameter will be deprecated in pyfar 0.8.0 in "
"favor of frequency_range")
def deconvolve(system_output, system_input, fft_length=None,
frequency_range=None, **kwargs):
r"""Calculate transfer functions by spectral deconvolution of two signals.

The transfer function :math:`H(\omega)` is calculated by spectral
Expand Down Expand Up @@ -1453,7 +1466,7 @@ def deconvolve(system_output, system_input, fft_length=None, freq_range=None,
The system input signal (e.g., used to perform a measurement).
The system input signal is zero padded, if it is shorter than the
system output signal.
freq_range : tuple, array_like, double
frequency_range : tuple, array_like, double
The upper and lower frequency limits outside of which the
regularization factor is to be applied. The default ``None``
bypasses the regularization, which might cause numerical
Expand All @@ -1467,6 +1480,14 @@ def deconvolve(system_output, system_input, fft_length=None, freq_range=None,
kwargs : key value arguments
Key value arguments to control the inversion of :math:`H(\omega)` are
passed to to :py:func:`~pyfar.dsp.regularized_spectrum_inversion`.
freq_range: tuple, array_like, double
The upper and lower frequency limits outside of which the
regularization factor is to be applied. The default ``None``
bypasses the regularization, which might cause numerical
instabilities in case of band-limited `system_input`. Also see
:py:func:`~pyfar.dsp.regularized_spectrum_inversion`.
``'freq_range'`` parameter will be deprecated in pyfar 0.8.0 in favor
of ``'frequency_range'``.


Returns
Expand All @@ -1482,7 +1503,6 @@ def deconvolve(system_output, system_input, fft_length=None, freq_range=None,
sweeps. Directors cut." J. Audio Eng. Soc. 49(6):443-471,
(2001, June).
"""

# Check if system_output and system_input are both type Signal
if not isinstance(system_output, pyfar.Signal):
raise TypeError('system_output has to be of type pyfar.Signal')
Expand All @@ -1493,8 +1513,8 @@ def deconvolve(system_output, system_input, fft_length=None, freq_range=None,
if not system_output.sampling_rate == system_input.sampling_rate:
raise ValueError("The two signals have different sampling rates!")

if freq_range is None:
freq_range = (0, system_input.sampling_rate/2)
if frequency_range is None:
frequency_range = (0, system_input.sampling_rate/2)

# Set fft_length to the max n_samples of both signals,
# if it is not explicitly set to a value
Expand All @@ -1518,7 +1538,7 @@ def deconvolve(system_output, system_input, fft_length=None, freq_range=None,
# multiply system_output signal with regularized inversed system_input
# signal to get the system response
inverse_input = regularized_spectrum_inversion(
system_input, freq_range, **kwargs)
system_input, frequency_range, **kwargs)
system_response = system_output * inverse_input

# Check if the signals have any comments,
Expand Down
23 changes: 17 additions & 6 deletions pyfar/dsp/filter/fractional_octaves.py
Expand Up @@ -2,6 +2,7 @@
import numpy as np
import scipy.signal as spsignal
import pyfar as pf
from pyfar._utils import rename_arg


def fractional_octave_frequencies(
Expand Down Expand Up @@ -153,11 +154,14 @@ def _center_frequencies_fractional_octaves_iec(nominal, num_fractions):
return nominal, exact


@rename_arg({"freq_range": "frequency_range"},
"freq_range parameter will be deprecated in pyfar 0.8.0 in "
"favor of frequency_range")
def fractional_octave_bands(
signal,
num_fractions,
sampling_rate=None,
freq_range=(20.0, 20e3),
frequency_range=(20.0, 20e3),
order=14):
"""Create and/or apply an energy preserving fractional octave filter bank.

Expand Down Expand Up @@ -189,11 +193,17 @@ def fractional_octave_bands(
sampling_rate : None, int
The sampling rate in Hz. Only required if signal is ``None``. The
default is ``None``.
freq_range : array, tuple, optional
frequency_range : array, tuple, optional
The lower and upper frequency limits. The default is
``frequency_range=(20, 20e3)``.
order : int, optional
Order of the Butterworth filter. The default is ``14``.
freq_range: array, tuple, optional
The lower and upper frequency limits. The default is
``frequency_range=(20, 20e3)``.
``'freq_range'`` parameter will be deprecated in pyfar 0.8.0 in favor
of ``'frequency_range'``.


Returns
-------
Expand All @@ -215,7 +225,7 @@ def fractional_octave_bands(
>>> # generate the data
>>> x = pf.signals.impulse(2**17)
>>> y = pf.dsp.filter.fractional_octave_bands(
... x, 1, freq_range=(20, 8e3))
... x, 1, frequency_range=(20, 8e3))
>>> # frequency domain plot
>>> y_sum = pf.FrequencyData(
... np.sum(np.abs(y.freq)**2, 0), y.frequencies)
Expand All @@ -225,6 +235,7 @@ def fractional_octave_bands(
... "Filter bands and the sum of their squared magnitudes")

"""

# check input
if (signal is None and sampling_rate is None) \
or (signal is not None and sampling_rate is not None):
Expand All @@ -234,7 +245,7 @@ def fractional_octave_bands(

sos = _coefficients_fractional_octave_bands(
sampling_rate=fs, num_fractions=num_fractions,
freq_range=freq_range, order=order)
frequency_range=frequency_range, order=order)

filt = pf.FilterSOS(sos, fs)
filt.comment = (
Expand All @@ -253,7 +264,7 @@ def fractional_octave_bands(

def _coefficients_fractional_octave_bands(
sampling_rate, num_fractions,
freq_range=(20.0, 20e3), order=14):
frequency_range=(20.0, 20e3), order=14):
"""Calculate the second order section filter coefficients of a fractional
octave band filter bank.

Expand Down Expand Up @@ -283,7 +294,7 @@ def _coefficients_fractional_octave_bands(
"""

f_crit = fractional_octave_frequencies(
num_fractions, freq_range, return_cutoff=True)[2]
num_fractions, frequency_range, return_cutoff=True)[2]

freqs_upper = f_crit[1]
freqs_lower = f_crit[0]
Expand Down