# 拉普拉斯变换零极点图实战:用Python可视化信号收敛域(附完整代码)
很多信号处理的学习者,包括当年的我自己,在初次接触拉普拉斯变换时,总会卡在一个关键环节:零极点图与收敛域(ROC)的关系。教科书上的理论推导严谨,但那些抽象的复平面、极点、收敛边界,总让人觉得隔着一层纱。直到我开始用代码把这一切画出来,那些原本模糊的概念才瞬间变得清晰可见。这篇文章,就是为你准备的“可视化工具箱”。我们不打算重复那些冗长的公式推导,而是直接动手,用Python和Matplotlib,一步步构建起从零极点分布到收敛域判断的完整视觉化流程。无论你是正在啃硬骨头的工科生,还是需要快速验证设计思路的工程师,这套代码和思路都能让你对信号系统的理解,从“知道”跃升到“看见”。
## 1. 环境准备与核心库介绍
在开始绘制零极点图之前,我们需要一个稳定、高效的Python环境。我推荐使用**Anaconda**来管理你的科学计算环境,它能避免很多依赖库版本冲突的麻烦。如果你习惯使用原生Python,请确保使用Python 3.8或更高版本。
接下来是核心库的安装。我们将主要依赖三个库:`NumPy`用于数值计算,`Matplotlib`用于绘图,`SciPy`中的`signal`模块则能方便地处理传递函数。打开你的终端或命令提示符,执行以下命令来安装它们:
```bash
pip install numpy matplotlib scipy
```
安装完成后,我们可以在Python脚本或Jupyter Notebook中导入这些库。我习惯在Jupyter Notebook中进行探索性编程,因为可以即时看到绘图结果,方便调试。
```python
import numpy as np
import matplotlib.pyplot as plt
from scipy import signal
import warnings
warnings.filterwarnings('ignore') # 忽略一些不影响绘图的警告
plt.rcParams['font.sans-serif'] = ['SimHei', 'DejaVu Sans'] # 解决中文显示问题
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
```
> 提示:如果你在绘图时遇到中文字体显示为方框的问题,上述代码中的字体设置(`SimHei`)可能不适用于你的系统。在macOS或Linux上,你可以尝试使用`‘Arial Unicode MS’`或`‘DejaVu Sans’`。
**为什么选择Matplotlib?** 在众多Python绘图库中,Matplotlib可能不是最“炫酷”的,但它绝对是科学绘图领域的基石。其优势在于:
- **高度可定制性**:从坐标轴刻度到图例样式,几乎每一个视觉元素都可以精细调整。
- **与NumPy无缝集成**:直接处理数组数据,绘图逻辑直观。
- **丰富的图表类型**:除了基础的散点图、线图,还支持极坐标、3D绘图等,非常适合复平面可视化。
准备好环境后,我们的“画布”和“颜料”就齐全了。接下来,让我们深入理解我们要描绘的对象——零极点图本身。
## 2. 理解零极点图:从抽象公式到复平面上的点
拉普拉斯变换将时域信号`x(t)`映射到复频域`X(s)`,其中`s = σ + jω`是一个复数。当`X(s)`可以表示为两个多项式之比(即有理函数)时,我们就能找到它的零点和极点。
- **零点**:使分子多项式`N(s) = 0`的`s`值。在图上,我们用**圆圈 (o)** 表示。
- **极点**:使分母多项式`D(s) = 0`的`s`值。在图上,我们用**叉号 (x)** 表示。
这些点落在复平面上,就构成了一幅零极点图。这幅图之所以强大,是因为它几乎“编码”了系统或信号的全部频域特性。例如,极点的位置决定了系统自然响应(如振荡频率、衰减速度),而零点的位置则影响频率响应的形状(如形成陷波)。
让我们从一个最简单的例子开始。考虑一个系统的传递函数为:
`H(s) = (s - 2) / ((s + 1 + 2j)(s + 1 - 2j))`
这个函数有一个实数零点`s = 2`,和一对共轭复数极点`s = -1 ± 2j`。
用代码来找出并绘制它们:
```python
# 定义分子和分母多项式的系数
# 分子: s - 2 -> 系数为 [1, -2] (代表 1*s^1 + (-2)*s^0)
# 分母: (s+1+2j)(s+1-2j) = s^2 + 2s + 5 -> 系数为 [1, 2, 5]
num = [1, -2] # 分子系数,按s的降幂排列
den = [1, 2, 5] # 分母系数
# 使用scipy.signal的tf2zpk函数直接获取零点和极点
zeros, poles, _ = signal.tf2zpk(num, den)
print(f"零点: {zeros}")
print(f"极点: {poles}")
```
运行这段代码,你会看到输出:
```
零点: [2.+0.j]
极点: [-1.+2.j -1.-2.j]
```
这验证了我们的手动计算。现在,让我们把它们画到复平面上。这里有一个关键的绘图技巧:为了清晰地展示实部(σ)和虚部(ω)轴,以及零极点的相对位置,我们需要精心设置坐标轴的范围和比例。
```python
def plot_pole_zero(zeros, poles, title='零极点图'):
"""
绘制零极点图的基础函数
"""
fig, ax = plt.subplots(figsize=(8, 8))
# 绘制零点(圆圈)和极点(叉号)
# 使用np.real和np.imag分别提取实部和虚部
ax.plot(np.real(zeros), np.imag(zeros), 'o', markersize=10, label='零点', fillstyle='none', markeredgewidth=2)
ax.plot(np.real(poles), np.imag(poles), 'x', markersize=10, label='极点', markeredgewidth=2)
# 设置坐标轴
ax.axhline(y=0, color='k', linestyle='-', alpha=0.3) # 实轴
ax.axvline(x=0, color='k', linestyle='-', alpha=0.3) # 虚轴
ax.set_xlabel('实部 (σ)')
ax.set_ylabel('虚部 (jω)')
ax.set_title(title)
ax.grid(True, linestyle='--', alpha=0.5)
ax.legend()
ax.set_aspect('equal') # 保证x轴和y轴比例相同,圆看起来才是圆
# 自动调整坐标轴范围,以包含所有零极点并留出一些边距
all_points = np.concatenate([zeros, poles])
if len(all_points) > 0:
real_part = np.real(all_points)
imag_part = np.imag(all_points)
margin = 1.0
ax.set_xlim([real_part.min() - margin, real_part.max() + margin])
ax.set_ylim([imag_part.min() - margin, imag_part.max() + margin])
plt.show()
# 调用函数绘图
plot_pole_zero(zeros, poles, title='示例系统零极点图')
```
执行这段代码,你将得到一张清晰的零极点图。图上,实轴(σ)代表衰减/增长因子,虚轴(jω)代表振荡频率。那个在σ=2处的圆圈是零点,而在σ=-1, ω=±2处的两个叉号是共轭极点。这幅静态的图,已经蕴含了丰富的动态信息。但我们的故事才刚刚开始,零极点图的真正威力,在于它与信号收敛域的深刻联系。
## 3. 收敛域(ROC)的可视化:划定信号的“存在”区域
收敛域是拉普拉斯变换中一个既关键又容易让人困惑的概念。简单说,它是复平面`S`上使拉普拉斯变换积分绝对收敛的所有`s`值的集合。**ROC不是由变换式`X(s)`本身唯一决定的,而是与时域信号`x(t)`的类型(右边、左边、双边或有限长)紧密相关。** 零极点图为我们判断ROC提供了完美的视觉框架。
基于零极点在复平面上的位置,ROC的规则可以直观地理解为:
| 信号类型 | ROC在复平面上的区域 | 判断关键 |
| :--- | :--- | :--- |
| **右边信号** (因果信号,t<0时x(t)=0) | 在最右边极点的**右侧**区域 | 向右延伸至无穷远 |
| **左边信号** (反因果信号,t>0时x(t)=0) | 在最左边极点的**左侧**区域 | 向左延伸至无穷远 |
| **双边信号** | 可能是一个**带状区域** | 位于两个相邻极点之间 |
| **有限长信号** (持续时间有限) | **整个复平面** | 没有ROC边界限制 |
让我们用代码来具象化这些规则。我们将编写一个函数,它不仅能画出零极点,还能根据指定的信号类型,用阴影高亮显示出对应的ROC区域。
```python
def plot_pole_zero_with_roc(zeros, poles, signal_type='right_sided', roc_boundary=None):
"""
绘制零极点图并高亮显示收敛域(ROC)
参数:
zeros: 零点数组
poles: 极点数组
signal_type: 信号类型,可选 'right_sided'(右边), 'left_sided'(左边), 'two_sided'(双边), 'finite_duration'(有限长)
roc_boundary: 对于双边信号,可以指定ROC的左右边界实部 [sigma_left, sigma_right]
"""
fig, ax = plt.subplots(figsize=(10, 8))
# 1. 绘制零极点
ax.plot(np.real(zeros), np.imag(zeros), 'bo', markersize=12, label='零点', fillstyle='none', markeredgewidth=2)
ax.plot(np.real(poles), np.imag(poles), 'rx', markersize=12, label='极点', markeredgewidth=3)
# 2. 根据信号类型绘制ROC区域
if len(poles) > 0:
poles_real = np.real(poles)
rightmost_pole = poles_real.max()
leftmost_pole = poles_real.min()
# 获取当前坐标轴范围用于填充
x_min, x_max = ax.get_xlim()
y_min, y_max = ax.get_ylim()
# 根据信号类型填充ROC区域
if signal_type == 'right_sided':
# 右边信号:最右边极点右侧
fill_x = [rightmost_pole, x_max, x_max, rightmost_pole]
fill_y = [y_min, y_min, y_max, y_max]
ax.fill(fill_x, fill_y, 'green', alpha=0.2, label='ROC (右边信号)')
# 在边界画一条虚线
ax.axvline(x=rightmost_pole, color='green', linestyle='--', linewidth=2, alpha=0.7)
elif signal_type == 'left_sided':
# 左边信号:最左边极点左侧
fill_x = [x_min, leftmost_pole, leftmost_pole, x_min]
fill_y = [y_min, y_min, y_max, y_max]
ax.fill(fill_x, fill_y, 'orange', alpha=0.2, label='ROC (左边信号)')
ax.axvline(x=leftmost_pole, color='orange', linestyle='--', linewidth=2, alpha=0.7)
elif signal_type == 'two_sided':
# 双边信号:带状区域,需要指定或自动选择两个极点
if roc_boundary is None:
# 自动找到实部相邻的两个极点作为边界
sorted_poles_real = np.sort(poles_real)
# 这里简单取中间两个极点(如果极点大于2个)
if len(sorted_poles_real) >= 2:
idx = len(sorted_poles_real) // 2
sigma_left = sorted_poles_real[idx-1]
sigma_right = sorted_poles_real[idx]
else:
# 如果极点少于2个,假设一个常见带状区域
sigma_left, sigma_right = -2, 0
else:
sigma_left, sigma_right = roc_boundary
fill_x = [sigma_left, sigma_right, sigma_right, sigma_left]
fill_y = [y_min, y_min, y_max, y_max]
ax.fill(fill_x, fill_y, 'purple', alpha=0.2, label='ROC (双边信号)')
# 画两条边界虚线
ax.axvline(x=sigma_left, color='purple', linestyle='--', linewidth=2, alpha=0.7)
ax.axvline(x=sigma_right, color='purple', linestyle='--', linewidth=2, alpha=0.7)
elif signal_type == 'finite_duration':
# 有限长信号:整个平面都是ROC
ax.fill_between([x_min, x_max], y_min, y_max, color='gray', alpha=0.1, label='ROC (整个平面)')
ax.text(0.5, 0.5, '整个平面均为ROC', transform=ax.transAxes,
ha='center', va='center', fontsize=12, style='italic')
# 3. 设置坐标轴和图形属性
ax.axhline(y=0, color='k', linestyle='-', alpha=0.3)
ax.axvline(x=0, color='k', linestyle='-', alpha=0.3)
ax.set_xlabel('实部 (σ)')
ax.set_ylabel('虚部 (jω)')
# 动态生成标题
type_map = {'right_sided': '右边信号', 'left_sided': '左边信号',
'two_sided': '双边信号', 'finite_duration': '有限长信号'}
title = f"零极点图与收敛域 - {type_map.get(signal_type, signal_type)}"
ax.set_title(title, fontsize=14)
ax.grid(True, linestyle='--', alpha=0.5)
ax.legend(loc='best')
ax.set_aspect('equal')
# 重新设置坐标轴范围,确保所有元素可见
all_points = np.concatenate([zeros, poles])
if len(all_points) > 0:
real_part = np.real(all_points)
imag_part = np.imag(all_points)
margin = 1.5
ax.set_xlim([real_part.min() - margin, real_part.max() + margin])
ax.set_ylim([imag_part.min() - margin, imag_part.max() + margin])
plt.tight_layout()
plt.show()
```
现在,让我们用几个具体的传递函数来测试这个强大的可视化工具。
**案例一:右边信号(因果系统)**
考虑一个简单的RC低通滤波器的传递函数:`H(s) = 1 / (s + 2)`。它只有一个极点`s = -2`。对于因果系统(右边信号),ROC是最右边极点(此处即唯一极点)的右侧,即`Re{s} > -2`。
```python
# 案例1: 右边信号 - RC低通滤波器
num1 = [1] # 分子为1
den1 = [1, 2] # 分母 s+2 -> [1, 2]
zeros1, poles1, _ = signal.tf2zpk(num1, den1)
plot_pole_zero_with_roc(zeros1, poles1, signal_type='right_sided')
```
运行后,你会看到在σ = -2处有一个红色的“x”(极点),而整个其右侧的区域被浅绿色阴影覆盖,这就是ROC。这条垂直的绿色虚线就是收敛边界。
**案例二:左边信号**
考虑一个反因果信号,其拉氏变换为`X(s) = 1 / (s - 1)`,极点`s = 1`。对于左边信号,ROC在最左边极点的左侧,即`Re{s} < 1`。
```python
# 案例2: 左边信号
num2 = [1]
den2 = [1, -1] # 分母 s-1 -> [1, -1]
zeros2, poles2, _ = signal.tf2zpk(num2, den2)
plot_pole_zero_with_roc(zeros2, poles2, signal_type='left_sided')
```
图中,极点位于σ=1处,橙色阴影覆盖了其左侧区域。
**案例三:双边信号与ROC的不唯一性**
这是最有趣也最具挑战性的情况。考虑`X(s) = 1 / ((s+1)(s-2))`,它有两个实极点:`s = -1` 和 `s = 2`。对于双边信号,ROC可以是这两个极点之间的任何带状区域,但具体是哪一条,取决于时域信号`x(t)`是左边部分和右边部分的何种组合。我们的函数允许你通过`roc_boundary`参数来指定。
```python
# 案例3: 双边信号 - 两种可能的ROC
num3 = [1]
den3 = [1, -1, -2] # (s+1)(s-2) = s^2 - s -2 -> [1, -1, -2]
zeros3, poles3, _ = signal.tf2zpk(num3, den3)
print(f"极点位于: {poles3}")
# 第一种可能的ROC: -1 < Re{s} < 2
plot_pole_zero_with_roc(zeros3, poles3, signal_type='two_sided', roc_boundary=[-1, 2])
# 第二种可能的ROC: Re{s} < -1 (假设信号是左边主导)
# plot_pole_zero_with_roc(zeros3, poles3, signal_type='two_sided', roc_boundary=[-5, -1])
# 第三种可能的ROC: Re{s} > 2 (假设信号是右边主导)
# plot_pole_zero_with_roc(zeros3, poles3, signal_type='two_sided', roc_boundary=[2, 5])
```
通过切换`roc_boundary`参数,你可以直观地看到ROC如何像一个“滑动窗口”在极点之间移动。这种可视化清晰地解释了为什么同一个`X(s)`可以对应多个不同的时域信号`x(t)`——关键就在于ROC的选择。
## 4. 综合实战:从零极点图反推系统特性与稳定性分析
掌握了零极点的绘制和ROC的标注,我们就可以玩点更高级的了。零极点图不仅是结果的展示,更是系统分析的起点。我们可以从图中直接“读”出系统的许多关键特性。
**4.1 判断系统稳定性**
对于因果系统(ROC是最右边极点的右侧),**稳定性要求所有极点都位于S平面的左半开平面(即极点的实部σ < 0)**。因为只有这样才能保证系统的冲激响应是衰减的。让我们写一个简单的判断函数:
```python
def check_stability(poles, system_type='causal'):
"""
根据极点位置判断系统稳定性
参数:
poles: 极点数组
system_type: 系统类型,'causal'为因果系统
"""
poles_real = np.real(poles)
if system_type == 'causal':
if len(poles) == 0:
return True, "无极点(全通或常数系统)"
elif np.all(poles_real < 0):
return True, "所有极点实部小于0,系统稳定"
elif np.any(poles_real > 0):
return False, f"存在极点实部大于0 (如 {poles_real[poles_real > 0][0]:.2f}),系统不稳定"
else: # 可能有极点实部等于0
# 检查虚轴上的极点是否为单极点
on_imag_axis = np.abs(poles_real) < 1e-10
if np.any(on_imag_axis):
# 简单判断:如果虚轴上的极点是单极点,可能临界稳定(如理想积分器、理想振荡器)
# 实际中需根据具体应用判断
return False, "极点在虚轴上,系统临界稳定或不稳定"
# 对于非因果系统,稳定性判断更复杂,此处省略
return None, "非因果系统稳定性判断未实现"
# 测试几个系统
test_cases = [
([1, 2, 5], "稳定系统(极点实部为负)"),
([1, -1, 2], "不稳定系统(有正实部极点)"),
([1, 0, 4], "临界稳定(极点在虚轴上)"), # s^2 + 4 = 0 -> s = ±2j
([1], "一阶稳定系统"), # s+1 -> 极点-1
]
for den_coeff, desc in test_cases:
_, poles, _ = signal.tf2zpk([1], den_coeff) # 假设分子为1
is_stable, reason = check_stability(poles, 'causal')
print(f"{desc}: 极点={poles}, 稳定? {is_stable}, 原因: {reason}")
```
**4.2 估算频率响应与谐振峰**
对于因果稳定系统,其频率响应`H(jω)`可以通过在虚轴(`s = jω`)上计算`H(s)`得到。零极点图给了我们一种几何估算频率响应幅度的方法:幅度响应`|H(jω)|`与从点`jω`到各零点的距离乘积成正比,与到各极点的距离乘积成反比。
当`jω`在复平面上沿着虚轴从下往上移动时:
- 如果`jω`非常靠近一个极点,那么到该极点的距离很小,`|H(jω)|`会很大,可能出现谐振峰。
- 如果`jω`非常靠近一个零点,那么到该零点的距离很小,`|H(jω)|`会很小,可能出现陷波(频率被抑制)。
我们可以编写一个函数,来可视化这种几何关系,并近似画出幅频响应曲线。
```python
def geometric_frequency_response(zeros, poles, omega_range=(-5, 5), num_points=1000):
"""
利用零极点图的几何方法估算频率响应幅度
"""
# 生成频率点
omega = np.linspace(omega_range[0], omega_range[1], num_points)
s = 1j * omega # 在虚轴上的点
# 初始化幅度数组
magnitude = np.ones_like(omega, dtype=complex)
# 几何计算:幅度 ∝ (到零点距离的乘积) / (到极点距离的乘积)
# 为避免除零,我们计算对数幅度
log_magnitude = np.zeros_like(omega, dtype=float)
for w_idx, s_point in enumerate(s):
# 计算到所有零点的距离
if len(zeros) > 0:
dist_to_zeros = np.abs(s_point - zeros)
log_magnitude[w_idx] += np.sum(np.log(dist_to_zeros))
# 计算到所有极点的距离
if len(poles) > 0:
dist_to_poles = np.abs(s_point - poles)
log_magnitude[w_idx] -= np.sum(np.log(dist_to_poles))
# 转换为线性幅度(并归一化以便于观察)
magnitude_approx = np.exp(log_magnitude)
magnitude_approx = magnitude_approx / np.max(magnitude_approx) # 归一化
# 绘图
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
# 左图:零极点图,并标记出虚轴
ax1 = axes[0]
ax1.plot(np.real(zeros), np.imag(zeros), 'bo', markersize=10, label='零点', fillstyle='none', markeredgewidth=2)
ax1.plot(np.real(poles), np.imag(poles), 'rx', markersize=10, label='极点', markeredgewidth=3)
# 高亮虚轴
ax1.axvline(x=0, color='green', linestyle='-', linewidth=2, alpha=0.5, label='虚轴 (s=jω)')
ax1.set_xlabel('实部 (σ)')
ax1.set_ylabel('虚部 (jω)')
ax1.set_title('零极点图与虚轴')
ax1.grid(True, linestyle='--', alpha=0.5)
ax1.legend()
ax1.set_aspect('equal')
# 设置合适的坐标范围
all_points = np.concatenate([zeros, poles, np.array([0+1j*omega_range[0], 0+1j*omega_range[1]])])
real_part = np.real(all_points)
imag_part = np.imag(all_points)
margin = 1.0
ax1.set_xlim([real_part.min() - margin, real_part.max() + margin])
ax1.set_ylim([imag_part.min() - margin, imag_part.max() + margin])
# 右图:估算的幅频响应
ax2 = axes[1]
ax2.plot(omega, 20 * np.log10(magnitude_approx + 1e-10), 'b-', linewidth=2) # 转换为dB
ax2.set_xlabel('角频率 ω (rad/s)')
ax2.set_ylabel('幅度 (dB)')
ax2.set_title('基于零极点几何关系的幅频响应估算')
ax2.grid(True, linestyle='--', alpha=0.5)
ax2.set_xlim(omega_range)
plt.tight_layout()
plt.show()
return omega, magnitude_approx
# 测试:一个带通滤波器特性的系统
# 极点: -0.5 ± 3j (在左半平面,靠近虚轴,会产生谐振)
# 零点: 0 (在原点,会在低频产生陷波?实际上,原点处的零点会使直流增益为0)
zeros_test = np.array([0.0])
poles_test = np.array([-0.5 + 3j, -0.5 - 3j])
omega, mag = geometric_frequency_response(zeros_test, poles_test, omega_range=(-5, 5))
```
运行这段代码,你会得到两幅并排的图。左图展示了零极点相对于虚轴的位置,右图则是估算出的幅频响应曲线。你可以尝试移动零极点的位置,比如把极点向虚轴靠近(实部更接近0),观察右图中的谐振峰如何变得更高更尖锐;或者把一个零点放在`jω=2`附近,观察是否在ω=2 rad/s附近出现一个凹陷。这种即时反馈,是理论学习难以提供的。
**4.3 完整案例:设计并分析一个二阶带阻滤波器**
让我们把所有知识串联起来,完成一个综合任务:设计一个简单的二阶带阻滤波器(陷波器),分析其零极点图、ROC、稳定性,并估算其频率响应。
假设我们希望抑制ω=4 rad/s的频率分量。一个经典的方法是在`jω=4`和`jω=-4`处放置一对共轭零点。为了保持系统因果稳定,我们在左半平面放置一对共轭极点,比如在`s = -1 ± 5j`。这样,传递函数大致为:
`H(s) = (s^2 + 16) / ((s+1)^2 + 25) = (s^2 + 16) / (s^2 + 2s + 26)`
```python
# 综合案例:二阶带阻滤波器设计与分析
print("="*50)
print("综合案例:二阶带阻滤波器分析")
print("="*50)
# 1. 定义系统
# 零点在 s = ±4j, 对应多项式 s^2 + 16
# 极点在 s = -1 ± 5j, 对应多项式 (s+1)^2 + 25 = s^2 + 2s + 26
num_notch = [1, 0, 16] # s^2 + 16
den_notch = [1, 2, 26] # s^2 + 2s + 26
zeros_notch, poles_notch, _ = signal.tf2zpk(num_notch, den_notch)
print(f"零点位置: {zeros_notch}")
print(f"极点位置: {poles_notch}")
# 2. 绘制零极点图与ROC(假设为因果系统)
plot_pole_zero_with_roc(zeros_notch, poles_notch, signal_type='right_sided')
# 3. 稳定性判断
is_stable, reason = check_stability(poles_notch, 'causal')
print(f"\n稳定性分析: {reason}")
# 4. 几何法估算频率响应
print("\n正在生成频率响应估算图...")
omega_notch, mag_notch = geometric_frequency_response(zeros_notch, poles_notch, omega_range=(-8, 8))
# 5. (可选)使用scipy.signal计算精确的频率响应进行对比
w, h = signal.freqs(num_notch, den_notch, worN=1000)
h_mag = np.abs(h)
h_mag_db = 20 * np.log10(h_mag + 1e-10)
h_mag_db_normalized = h_mag_db - np.max(h_mag_db) # 归一化到0dB
# 绘制对比图
fig, ax = plt.subplots(figsize=(10, 6))
ax.plot(omega_notch, 20*np.log10(mag_notch + 1e-10), 'b--', linewidth=2, alpha=0.7, label='几何估算 (归一化)')
ax.plot(w, h_mag_db_normalized, 'r-', linewidth=1.5, label='精确计算 (freqs)')
ax.set_xlabel('角频率 ω (rad/s)')
ax.set_ylabel('幅度 (dB)')
ax.set_title('带阻滤波器幅频响应:几何估算 vs. 精确计算')
ax.grid(True, linestyle='--', alpha=0.5)
ax.legend()
ax.set_xlim([-8, 8])
plt.tight_layout()
plt.show()
print("\n分析完成。可以看到,在零点位置ω=±4 rad/s附近,幅度响应出现明显的凹陷(陷波)。")
print("极点位于左半平面,保证了系统的因果稳定性。")
```
通过这个完整案例,你不仅看到了如何从零开始用代码定义一个系统,还实践了从零极点图出发,进行ROC判断、稳定性分析,以及频率响应估算的全流程。这种将理论、可视化和代码验证结合的方法,能极大地加深你对信号与系统核心概念的理解。
可视化不是理论的替代品,而是理解理论的桥梁。当你下次在课本上看到抽象的零极点分布图时,希望你的第一反应是:“我可以用Python把它画出来,并看看它的ROC和频率响应是什么样子。” 这套代码和思路就是一个起点,你可以修改参数,设计不同的零极点配置,观察它们如何影响系统的时域和频域行为。真正的掌握,始于亲手实践和不断探索。