Skip to content

Commit

Permalink
Remove frequency_response_log()
Browse files Browse the repository at this point in the history
Fixes #17, #168
  • Loading branch information
mhostetter committed Dec 3, 2023
1 parent 3b178f5 commit ba8d459
Show file tree
Hide file tree
Showing 4 changed files with 197 additions and 60 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"exponentials",
"feedforward",
"figsize",
"freqs",
"frombuffer",
"fspl",
"hann",
Expand Down
47 changes: 47 additions & 0 deletions src/sdr/_filter/_common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import Any

import numpy as np
import numpy.typing as npt
import scipy.signal


def get_frequency_vector(
freqs: int | float | npt.ArrayLike = 1024,
sample_rate: float = 1.0,
whole: bool = True,
decades: int | None = None,
) -> npt.NDArray[np.float_]:
if isinstance(freqs, int):
# freqs represents the number of frequency points
if whole:
max_f = sample_rate
else:
max_f = sample_rate / 2

if decades is None:
f = np.linspace(0, max_f, freqs, endpoint=False)
elif isinstance(decades, int):
f = np.logspace(np.log10(max_f) - decades, np.log10(max_f), freqs, endpoint=False)
else:
raise TypeError(f"Argument 'decades' must be an integer, not {type(decades).__name__}.")
else:
# freqs represents the single frequency or multiple frequencies
f = np.asarray(freqs, dtype=float)
f = np.atleast_1d(f)
if not f.ndim <= 1:
raise ValueError(f"Argument 'freqs' must be 0-D or 1-D, not {f.ndim}-D.")

return f


def frequency_response(
b: npt.ArrayLike,
a: npt.ArrayLike,
freqs: int | float | npt.ArrayLike = 1024,
sample_rate: float = 1.0,
whole: bool = True,
decades: int | None = None,
) -> Any:
f = get_frequency_vector(freqs, sample_rate, whole, decades)
f, H = scipy.signal.freqz(b, a, worN=f, fs=sample_rate)
return f, H
104 changes: 74 additions & 30 deletions src/sdr/_filter/_fir.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
"""
from __future__ import annotations

from typing import Any, overload

import numpy as np
import numpy.typing as npt
import scipy.signal
from typing_extensions import Literal

from .._helper import export
from ._common import frequency_response


@export
Expand Down Expand Up @@ -261,57 +264,98 @@ def step_response(self, N: int | None = None) -> npt.NDArray:

return s

def frequency_response(self, sample_rate: float = 1.0, N: int = 1024) -> tuple[npt.NDArray, npt.NDArray]:
@overload
def frequency_response(
self,
freqs: int = 1024,
sample_rate: float = 1.0,
whole: bool = True,
decades: int | None = None,
) -> tuple[npt.NDArray[np.float_], npt.NDArray[np.complex_]]:
...

@overload
def frequency_response(
self,
freqs: float,
sample_rate: float = 1.0,
) -> complex:
...

@overload
def frequency_response(
self,
freqs: npt.NDArray[np.float_],
sample_rate: float = 1.0,
) -> npt.NDArray[np.complex_]:
...

def frequency_response(
self,
freqs: Any = 1024,
sample_rate: Any = 1.0,
whole: Any = True,
decades: Any | None = None,
) -> Any:
r"""
Returns the frequency response $H(\omega)$ of the FIR filter.
Arguments:
freqs: The frequency specification.
- `int`: The number of frequency points. The endpoint is not included.
- `float`: A single frequency.
- `npt.NDArray[float]`: Multiple frequencies.
sample_rate: The sample rate $f_s$ of the filter in samples/s.
N: The number of samples in the frequency response.
whole: Only used if `freqs` is an integer.
- `True`: The maximum frequency is `max_f = sample_rate`.
- `False`: The maximum frequency is `max_f = sample_rate / 2`.
decades: Only used if `freqs` is an integer.
- `None`: `f = np.linspace(0, max_f, freqs, endpoint=False)`.
- `int`: `f = np.logspace(np.log10(max_f) - decades), np.log10(max_f), freqs, endpoint=False)`.
Returns:
- The frequencies $f$ from $-f_s/2$ to $f_s/2$ in Hz.
- The frequency vector $f$, only if `freqs` is an integer.
- The frequency response of the FIR filter $H(\omega)$.
See Also:
sdr.plot.magnitude_response, sdr.plot.phase_response
Examples:
See the :ref:`fir-filters` example.
"""
f, H = scipy.signal.freqz(self.taps, 1, worN=N, whole=True, fs=sample_rate)
.. ipython:: python
f[f >= 0.5 * sample_rate] -= sample_rate # Wrap frequencies from [0, 1) to [-0.5, 0.5)
f = np.fft.fftshift(f)
H = np.fft.fftshift(H)
h = sdr.design_lowpass_fir(100, 0.2, window="hamming"); \
fir = sdr.FIR(h)
return f, H
Compute the frequency response at 1024 evenly-spaced frequencies.
def frequency_response_log(
self, sample_rate: float = 1.0, N: int = 1024, decades: int = 4
) -> tuple[npt.NDArray, npt.NDArray]:
r"""
Returns the frequency response $H(\omega)$ of the FIR filter on a logarithmic frequency axis.
.. ipython:: python
Arguments:
sample_rate: The sample rate $f_s$ of the filter in samples/s.
N: The number of samples in the frequency response.
decades: The number of frequency decades to plot.
fir.frequency_response()
Returns:
- The frequencies $f$ from $0$ to $f_s/2$ in Hz. The frequencies are logarithmically-spaced.
- The frequency response of the IIR filter $H(\omega)$.
Compute the frequency response at 0.0 rad/s.
See Also:
sdr.plot.magnitude_response, sdr.plot.phase_response
.. ipython:: python
Examples:
See the :ref:`fir-filters` example.
"""
f = np.logspace(np.log10(sample_rate / 2 / 10**decades), np.log10(sample_rate / 2), N)
f, H = scipy.signal.freqz(self.taps, 1, worN=f, whole=False, fs=sample_rate)
fir.frequency_response(0.0)
return f, H
Compute the frequency response at several frequencies in Hz.
.. ipython:: python
fir.frequency_response([100, 200, 300, 400], sample_rate=1000)
"""
f, H = frequency_response(self.taps, 1, freqs, sample_rate, whole, decades)
if isinstance(freqs, int):
return f, H
elif isinstance(freqs, float):
return H[0]
else:
return H

def group_delay(self, sample_rate: float = 1.0, N: int = 1024) -> tuple[npt.NDArray, npt.NDArray]:
r"""
Expand Down
105 changes: 75 additions & 30 deletions src/sdr/_filter/_iir.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,15 @@
"""
from __future__ import annotations

from typing import Any, overload

import numpy as np
import numpy.typing as npt
import scipy.signal
from typing_extensions import Self

from .._helper import export
from ._common import frequency_response


@export
Expand Down Expand Up @@ -250,57 +253,99 @@ def step_response(self, N: int = 100) -> npt.NDArray:

return s

def frequency_response(self, sample_rate: float = 1.0, N: int = 1024) -> tuple[npt.NDArray, npt.NDArray]:
@overload
def frequency_response(
self,
freqs: int = 1024,
sample_rate: float = 1.0,
whole: bool = True,
decades: int | None = None,
) -> tuple[npt.NDArray[np.float_], npt.NDArray[np.complex_]]:
...

@overload
def frequency_response(
self,
freqs: float | npt.NDArray[np.float_],
sample_rate: float = 1.0,
) -> complex:
...

@overload
def frequency_response(
self,
freqs: npt.NDArray[np.float_],
sample_rate: float = 1.0,
) -> npt.NDArray[np.complex_]:
...

def frequency_response(
self,
freqs: Any = 1024,
sample_rate: Any = 1.0,
whole: Any = True,
decades: Any | None = None,
) -> Any:
r"""
Returns the frequency response $H(\omega)$ of the IIR filter.
Arguments:
freqs: The frequency specification.
- `int`: The number of frequency points. The endpoint is not included.
- `float`: A single frequency.
- `npt.NDArray[float]`: Multiple frequencies.
sample_rate: The sample rate $f_s$ of the filter in samples/s.
N: The number of samples in the frequency response.
whole: Only used if `freqs` is an integer.
- `True`: The maximum frequency is `max_f = sample_rate`.
- `False`: The maximum frequency is `max_f = sample_rate / 2`.
decades: Only used if `freqs` is an integer.
- `None`: `f = np.linspace(0, max_f, freqs, endpoint=False)`.
- `int`: `f = np.logspace(np.log10(max_f) - decades), np.log10(max_f), freqs, endpoint=False)`.
Returns:
- The frequencies $f$ from $-f_s/2$ to $f_s/2$ in Hz.
- The frequency vector $f$, only if `freqs` is an integer.
- The frequency response of the IIR filter $H(\omega)$.
See Also:
sdr.plot.magnitude_response, sdr.plot.phase_response
Examples:
See the :ref:`iir-filters` example.
"""
w, H = scipy.signal.freqz(self.b_taps, self.a_taps, worN=N, whole=True, fs=sample_rate) # type: ignore
.. ipython:: python
w[w >= 0.5 * sample_rate] -= sample_rate # Wrap frequencies from [0, 1) to [-0.5, 0.5)
w = np.fft.fftshift(w)
H = np.fft.fftshift(H)
zero = 0.6; \
pole = 0.8 * np.exp(1j * np.pi / 8); \
iir = sdr.IIR.ZerosPoles([zero], [pole, pole.conj()])
return w, H
Compute the frequency response at 1024 evenly-spaced frequencies.
def frequency_response_log(
self, sample_rate: float = 1.0, N: int = 1024, decades: int = 4
) -> tuple[npt.NDArray, npt.NDArray]:
r"""
Returns the frequency response $H(\omega)$ of the IIR filter on a logarithmic frequency axis.
.. ipython:: python
Arguments:
sample_rate: The sample rate $f_s$ of the filter in samples/s.
N: The number of samples in the frequency response.
decades: The number of frequency decades to plot.
iir.frequency_response()
Returns:
- The frequencies $f$ from $0$ to $f_s/2$ in Hz. The frequencies are logarithmically-spaced.
- The frequency response of the IIR filter $H(\omega)$.
Compute the frequency response at 0.0 rad/s.
See Also:
sdr.plot.magnitude_response, sdr.plot.phase_response
.. ipython:: python
Examples:
See the :ref:`iir-filters` example.
"""
w = np.logspace(np.log10(sample_rate / 2 / 10**decades), np.log10(sample_rate / 2), N)
w, H = scipy.signal.freqz(self.b_taps, self.a_taps, worN=w, whole=False, fs=sample_rate) # type: ignore
iir.frequency_response(0.0)
Compute the frequency response at several frequencies in Hz.
return w, H
.. ipython:: python
iir.frequency_response([100, 200, 300, 400], sample_rate=1000)
"""
f, H = frequency_response(self.b_taps, self.a_taps, freqs, sample_rate, whole, decades)
if isinstance(freqs, int):
return f, H
elif isinstance(freqs, float):
return H[0]
else:
return H

##############################################################################
# Properties
Expand Down

0 comments on commit ba8d459

Please sign in to comment.