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

Add sdr.plot subpackage #8

Merged
merged 20 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
2b1332f
Add a time-domain plot in `sdr.plot.time_domain()`
mhostetter Jul 11, 2023
b287847
Add `matplotlib` rc params
mhostetter Jul 11, 2023
439d1b2
Use rc params for time-domain plot
mhostetter Jul 11, 2023
0d277a6
Add `sdr.plot.use_style()` function
mhostetter Jul 12, 2023
55c4df9
Fix exported module for subpackages
mhostetter Jul 12, 2023
2d585c5
Remove disabling plot margins
mhostetter Jul 12, 2023
6308cf7
Add labels for complex data by default
mhostetter Jul 12, 2023
9840077
Add function to plot impulse response in `sdr.plot.impulse_response()`
mhostetter Jul 12, 2023
ea0afde
Add function to plot the step response in `sdr.plot.step_response()`
mhostetter Jul 12, 2023
d07919a
Add zeros and poles plot in `sdr.plot.zeros_and_poles()`
mhostetter Jul 12, 2023
9b10378
Add frequency response plot in `sdr.plot.frequency_response()`
mhostetter Jul 13, 2023
8f90733
Add group delay plot in `sdr.plot.group_delay()`
mhostetter Jul 13, 2023
1f1914f
Rename `sdr.plot.zeros_and_poles()` to `sdr.plot.zeros_poles()`
mhostetter Jul 13, 2023
03e7237
Add generic filter plot in `sdr.plot.filter()`
mhostetter Jul 13, 2023
95f44dd
Remove the plot methods from the IIR class
mhostetter Jul 13, 2023
7110b03
Fix pylint errors
mhostetter Jul 13, 2023
c37ad4c
Update notebooks with new plot functions
mhostetter Jul 13, 2023
508f98b
Suppress `scipy` warning about leading zeros in the numerator
mhostetter Jul 13, 2023
2bbaf51
Update examples
mhostetter Jul 13, 2023
33420cc
Fix linter errors
mhostetter Jul 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@ Phase/time-locked loops
-----------------------

.. python-apigen-group:: pll

Plotting
--------

.. python-apigen-group:: plotting
2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@
# Python apigen configuration
python_apigen_modules = {
"sdr": "api/sdr.",
# "sdr.typing": "api/sdr.typing.",
"sdr.plot": "api/sdr.plot.",
}
python_apigen_default_groups = [
("class:.*", "Classes"),
Expand Down
146 changes: 93 additions & 53 deletions docs/examples/iir-filter.ipynb

Large diffs are not rendered by default.

52 changes: 15 additions & 37 deletions docs/examples/phase-locked-loop.ipynb

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/sdr/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"Defaulting version to 0.0.0."
)

from . import plot
from ._farrow import *
from ._iir_filter import *
from ._loop_filter import *
Expand Down
7 changes: 4 additions & 3 deletions src/sdr/_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,10 @@ def export(obj):
module = sys.modules[obj.__module__]

if not SPHINX_BUILD:
# Set the object's module to the package name. This way the REPL will display the object
# as sdr.obj and not sdr._private_module.obj
obj.__module__ = "sdr"
# Set the object's module to the first non-private module. This way the REPL will display the object
# as sdr.obj and not sdr._private_module.obj.
idx = obj.__module__.find("._")
obj.__module__ = obj.__module__[:idx]

# Append this object to the private module's "all" list
public_members = getattr(module, "__all__", [])
Expand Down
185 changes: 2 additions & 183 deletions src/sdr/_iir_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
"""
from typing import Tuple

import matplotlib.pyplot as plt
import numpy as np
import scipy.signal
from typing_extensions import Self
Expand Down Expand Up @@ -41,8 +40,8 @@ def __init__(self, b: np.ndarray, a: np.ndarray, streaming: bool = False):
Creates an IIR filter with feedforward coefficients $b_i$ and feedback coefficients $a_j$.

Arguments:
b: Feedforward coefficients, $b_i$.
a: Feedback coefficients, $a_j$.
b: The feedforward coefficients, $b_i$.
a: The feedback coefficients, $a_j$.
streaming: Indicates whether to use streaming mode. In streaming mode, previous inputs are
preserved between calls to :meth:`filter()`.

Expand Down Expand Up @@ -204,186 +203,6 @@ def frequency_response_log(

return w, H

def plot_impulse_response(self, N: int = 100):
"""
Plots the impulse response $h[n]$ of the IIR filter.

Arguments:
N: The number of samples in the impulse response.

Examples:
See the :ref:`iir-filter` example.
"""
h = self.impulse_response(N)

# plt.stem(np.arange(h.size), h.real, linefmt="b-", markerfmt="bo")
plt.plot(np.arange(h.size), h.real, color="b", marker=".", label="Real")
plt.plot(np.arange(h.size), h.imag, color="r", marker=".", label="Imaginary")
plt.legend(loc="upper right")
plt.xlabel("Sample")
plt.ylabel("Amplitude")
plt.title("Impulse Response, $h[n]$")

def plot_step_response(self, N: int = 100):
"""
Plots the step response $s[n]$ of the IIR filter.

Arguments:
N: The number of samples in the step response.

Examples:
See the :ref:`iir-filter` example.
"""
u = self.step_response(N)

# plt.stem(np.arange(u.size), u.real, linefmt="b-", markerfmt="bo")
plt.plot(np.arange(u.size), u.real, color="b", marker=".", label="Real")
plt.plot(np.arange(u.size), u.imag, color="r", marker=".", label="Imaginary")
plt.legend(loc="lower right")
plt.xlabel("Sample")
plt.ylabel("Amplitude")
plt.title("Step Response, $s[n]$")

def plot_zeros_poles(self):
"""
Plots the zeros and poles of the IIR filter.

Examples:
See the :ref:`iir-filter` example.
"""
unit_circle = np.exp(1j * np.linspace(0, 2 * np.pi, 100))
z = self.zeros
p = self.poles

plt.plot(unit_circle.real, unit_circle.imag, color="k", linestyle="--", label="Unit circle")
plt.scatter(z.real, z.imag, color="b", marker="o", facecolor="none", label="Zeros")
plt.scatter(p.real, p.imag, color="r", marker="x", label="Poles")
plt.axis("equal")
plt.legend(loc="upper left")
plt.xlabel("Real")
plt.ylabel("Imaginary")
plt.title("Zeros and Poles of $H(z)$")

def plot_frequency_response(self, sample_rate: float = 1.0, N: int = 1024, phase: bool = True):
r"""
Plots the frequency response $H(\omega)$ of the IIR filter.

Arguments:
sample_rate: The sample rate of the filter in samples/s.
N: The number of samples in the frequency response.
phase: Indicates whether to plot the phase of $H(\omega)$.

Examples:
See the :ref:`iir-filter` example.
"""
w, H = self.frequency_response(sample_rate, N)

ax1 = plt.gca()
ax1.plot(w, 10 * np.log10(np.abs(H) ** 2), color="b", label="Power")
ax1.set_ylabel(r"Power (dB), $|H(\omega)|^2$")
ax1.tick_params(axis="y", labelcolor="b")
ax1.grid(which="both", linestyle="--")
if sample_rate == 1.0:
ax1.set_xlabel("Normalized Frequency, $f /f_s$")
else:
ax1.set_xlabel("Frequency (Hz), $f$")

if phase:
ax2 = ax1.twinx()
ax2.plot(w, np.rad2deg(np.angle(H)), color="r", linestyle="--", label="Phase")
ax2.set_ylabel(r"Phase (degrees), $\angle H(\omega)$")
ax2.tick_params(axis="y", labelcolor="r")
ax2.set_ylim(-180, 180)

plt.title(r"Frequency Response, $H(\omega)$")
plt.tight_layout()

def plot_frequency_response_log(
self, sample_rate: float = 1.0, N: int = 1024, phase: bool = True, decades: int = 4
):
r"""
Plots the frequency response $H(\omega)$ of the IIR filter on a logarithmic frequency axis.

Arguments:
sample_rate: The sample rate of the filter in samples/s.
N: The number of samples in the frequency response.
phase: Indicates whether to plot the phase of $H(\omega)$.
decades: The number of frequency decades to plot.

Examples:
See the :ref:`iir-filter` example.
"""
w, H = self.frequency_response_log(sample_rate, N, decades)

ax1 = plt.gca()
ax1.semilogx(w, 10 * np.log10(np.abs(H) ** 2), color="b", label="Power")
ax1.set_ylabel(r"Power (dB), $|H(\omega)|^2$")
ax1.tick_params(axis="y", labelcolor="b")
ax1.grid(which="both", linestyle="--")
if sample_rate == 1.0:
ax1.set_xlabel("Normalized Frequency, $f /f_s$")
else:
ax1.set_xlabel("Frequency (Hz), $f$")

if phase:
ax2 = ax1.twinx()
ax2.semilogx(w, np.rad2deg(np.angle(H)), color="r", linestyle="--", label="Phase")
ax2.set_ylabel(r"Phase (degrees), $\angle H(\omega)$")
ax2.tick_params(axis="y", labelcolor="r")
ax2.set_ylim(-180, 180)

plt.title(r"Frequency Response, $H(\omega)$")
plt.tight_layout()

def plot_group_delay(self, sample_rate: float = 1.0, N: int = 1024):
r"""
Plots the group delay $\tau_g(\omega)$ of the IIR filter.

Arguments:
sample_rate: The sample rate of the filter in samples/s.
N: The number of samples in the frequency response.

Examples:
See the :ref:`iir-filter` example.
"""
w, tau_g = scipy.signal.group_delay((self.b_taps, self.a_taps), w=N, whole=True, fs=sample_rate)

w[w >= 0.5 * sample_rate] -= sample_rate
w = np.fft.fftshift(w)
tau_g = np.fft.fftshift(tau_g)

plt.plot(w, tau_g, color="b")
if sample_rate == 1.0:
plt.xlabel("Normalized Frequency, $f /f_s$")
else:
plt.xlabel("Frequency (Hz), $f$")
plt.ylabel(r"Group Delay (samples), $\tau_g(\omega)$")
plt.title(r"Group Delay, $\tau_g(\omega)$")
plt.grid(which="both", linestyle="--")
plt.tight_layout()

def plot_all(self, sample_rate: float = 1.0, N_time: int = 100, N_freq: int = 1024):
"""
Plots the zeros and poles, impulse response, step response, and frequency response of the IIR filter
in a single figure.

Arguments:
sample_rate: The sample rate of the filter in samples/s.
N_time: The number of samples in the impulse and step responses.
N_freq: The number of samples in the frequency response.

Examples:
See the :ref:`iir-filter` example.
"""
plt.subplot2grid((4, 3), (0, 0), 2, 1)
self.plot_zeros_poles()
plt.subplot2grid((4, 3), (0, 1), 1, 2)
self.plot_impulse_response(N=N_time)
plt.subplot2grid((4, 3), (1, 1), 1, 2)
self.plot_step_response(N=N_time)
plt.subplot2grid((4, 3), (2, 0), 2, 3)
self.plot_frequency_response(sample_rate=sample_rate, N=N_freq)

@property
def b_taps(self) -> np.ndarray:
"""
Expand Down
5 changes: 3 additions & 2 deletions src/sdr/_pll.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def __init__(
K1 = lf.K1
K2 = lf.K2

b0 = 0
# b0 = 0
b1 = Kp * K0 * (K1 + K2)
b2 = -Kp * K0 * K1

Expand All @@ -92,7 +92,8 @@ def __init__(
a2 = 1 - Kp * K0 * K1

# Create an IIR filter that represents the closed-loop transfer function of the PLL
self._iir = IIR([b0, b1, b2], [a0, a1, a2])
# self._iir = IIR([b0, b1, b2], [a0, a1, a2])
self._iir = IIR([b1, b2], [a0, a1, a2]) # Suppress warning about leading zero

self._sample_rate = sample_rate
self._BnT = noise_bandwidth
Expand Down
6 changes: 6 additions & 0 deletions src/sdr/plot/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""
A subpackage for various plotting functions using :obj:`matplotlib`.
"""
from ._filter import * # pylint: disable=redefined-builtin
from ._rc_params import *
from ._time_domain import *
Loading