Source code for engineeringkit.signal_processing.visualizers.plots

"""Plotting helpers for engineeringkit.signal_processing: magnitude responses and pole-zero maps."""

from __future__ import annotations

import matplotlib.pyplot as plt
import numpy as np

from engineeringkit._validation import positive as _positive
from engineeringkit.signal_processing.systems.filters import frequency_response

__all__ = ["plot_magnitude_response", "plot_pole_zero"]


[docs] def plot_magnitude_response(b, a=1, fs=2 * np.pi, n=2048, ax=None, label=None): """Plot a digital filter's gain in decibels from DC to the Nyquist frequency. Parameters ---------- b, a : array_like Filter polynomials, or second-order sections in ``b`` with ``a=None``, as in :func:`~engineeringkit.signal_processing.frequency_response`. fs : float, optional Sampling rate [Hz]; the default :math:`2\\pi` gives rad/sample. n : int, optional Number of frequencies. ax : matplotlib.axes.Axes, optional Axes to draw on; a new figure is created if omitted. label : str, optional Legend label. Returns ------- matplotlib.axes.Axes """ _positive(fs=fs) f, H = frequency_response(b, a, worN=n, fs=fs) if ax is None: _, ax = plt.subplots() ax.plot(f, 20 * np.log10(np.maximum(np.abs(H), 1e-12)), label=label) ax.set(xlabel="frequency [Hz]" if fs != 2 * np.pi else "frequency [rad/sample]", ylabel="gain [dB]") ax.grid(True, alpha=0.3) if label: ax.legend() return ax
[docs] def plot_pole_zero(b, a, ax=None): """Plot the zeros (o) and poles (x) of :math:`B(z)/A(z)` with the unit circle. Parameters ---------- b, a : array_like Numerator and denominator coefficients in :math:`z^{-1}` [-]. ax : matplotlib.axes.Axes, optional Axes to draw on; a new figure is created if omitted. Returns ------- matplotlib.axes.Axes """ b, a = np.atleast_1d(np.asarray(b, dtype=float)), np.atleast_1d(np.asarray(a, dtype=float)) if b.ndim != 1 or a.ndim != 1 or not np.any(a) or not np.any(b): raise ValueError("b and a must be nonzero 1D coefficient vectors") # Pad to equal length so roots in z^-1 become roots in z, including those at the origin. n = max(b.size, a.size) zeros, poles = np.roots(np.pad(b, (0, n - b.size))), np.roots(np.pad(a, (0, n - a.size))) if ax is None: _, ax = plt.subplots(figsize=(4, 4)) theta = np.linspace(0, 2 * np.pi, 361) ax.plot(np.cos(theta), np.sin(theta), color="gray", lw=0.8) ax.plot(zeros.real, zeros.imag, "o", mfc="none", label="zeros") ax.plot(poles.real, poles.imag, "x", label="poles") ax.set_aspect("equal") ax.set(xlabel="real", ylabel="imaginary") ax.legend() return ax