# 手把手教你用Python绘制伯德图:从比例环节到振荡环节的完整实现
在控制系统设计与分析领域,伯德图(Bode Plot)无疑是一把利器。它以一种直观的图形化方式,揭示了系统在不同频率下的增益与相位变化,让工程师能够“看见”系统的频率响应特性。无论是评估系统稳定性、设计补偿器,还是调试滤波器参数,伯德图都扮演着核心角色。过去,绘制伯德图往往依赖于昂贵的专业软件或繁琐的手工计算,这让许多学习者和实践者望而却步。
如今,借助Python强大的开源生态,我们完全可以在熟悉的编程环境中,高效、灵活且精准地完成从简单比例环节到复杂振荡环节的伯德图绘制与分析。这篇文章正是为你——一位希望将理论付诸实践的工程师或学生——准备的实战指南。我们将彻底抛开枯燥的理论推导,聚焦于**可执行的代码、可复现的步骤以及可规避的陷阱**。你将学会如何利用 `control` 和 `matplotlib` 等库,从零开始构建典型环节的伯德图,并掌握让图表更具专业表现力的可视化技巧。让我们直接进入代码世界,用Python来“聆听”系统的频率之声。
## 1. 环境搭建与核心库初探
工欲善其事,必先利其器。在开始绘制伯德图之前,我们需要一个稳定且功能齐全的Python工作环境。我强烈推荐使用 **Anaconda** 来管理你的Python发行版和包依赖,它能极大减少环境配置带来的麻烦。如果你习惯使用纯Python的 `venv`,也完全没问题,只需确保能顺利安装后续的库即可。
首先,让我们通过命令行安装本次实战所需的几个核心库。打开你的终端(Windows的CMD/PowerShell,macOS/Linux的Terminal),执行以下命令:
```bash
pip install numpy matplotlib control slycot
```
这里对每个库做个简要说明:
* **NumPy**: Python科学计算的基础,提供高效的数组操作,是后续所有数学运算的基石。
* **Matplotlib**: Python绘图领域的“事实标准”,我们将用它来定制和美化伯德图。
* **Control**: 本次的主角,一个专门用于控制系统分析与设计的Python库。它提供了创建系统模型、计算频率响应和绘制伯德图等一站式功能。
* **Slycot** (可选但推荐): `control` 库在某些高级功能(如模型降阶、鲁棒控制分析)上依赖于 `slycot`。虽然绘制基础伯德图不一定需要,但预先安装可以避免未来可能出现的兼容性问题。
安装完成后,让我们在Python脚本或Jupyter Notebook中导入它们,并进行一个简单的功能验证。
```python
import numpy as np
import matplotlib.pyplot as plt
import control as ct
# 打印库版本,确认安装成功
print(f"NumPy version: {np.__version__}")
print(f"Matplotlib version: {plt.matplotlib.__version__}")
print(f"Control version: {ct.__version__}")
# 创建一个最简单的比例环节系统:G(s) = 5
sys_proportional = ct.tf([5], [1]) # tf([分子系数], [分母系数])
print(f"创建的系统: {sys_proportional}")
```
如果运行后能看到类似 `NumPy version: 1.24.3` 和 `创建的系统: 5` 的输出,那么恭喜你,环境已经准备就绪。`ct.tf([5], [1])` 这行代码创建了一个传递函数为5的系统对象,这是我们接触的第一个系统模型。`control` 库使用 `tf` (传递函数)、`ss` (状态空间) 等函数来构建系统,非常直观。
> 注意:如果你在安装 `control` 或 `slycot` 时遇到困难,特别是在Windows系统上,可以尝试先安装 `conda install -c conda-forge control`(如果你使用Anaconda),或者查阅 `control` 库的官方GitHub页面寻找针对特定操作系统的安装指南。
## 2. 典型环节的伯德图绘制实战
掌握了基础工具,我们现在可以逐个攻破控制系统中的典型环节。我会为每个环节提供完整的代码示例,并解释关键参数和常见输出的含义。
### 2.1 比例与积分微分环节:直线的艺术
比例、积分和微分环节是构成复杂系统的基本单元,它们的伯德图特征非常鲜明。
**比例环节 (G(s) = K)**
比例环节的伯德图最为简单:幅频图是一条平行于频率轴的直线,高度为20*log10(K) dB;相频图则是0度线。
```python
# 绘制不同K值的比例环节伯德图
K_values = [0.1, 1, 10] # 分别代表衰减、不变、放大
plt.figure(figsize=(10, 6))
for i, K in enumerate(K_values):
sys = ct.tf([K], [1])
# mag, phase, omega = ct.bode(sys, plot=False) 也可以获取数据不自绘
plt.subplot(2, 1, 1)
ct.bode_plot(sys, omega=np.logspace(-2, 2, 500), dB=True, Hz=False,
label=f'K={K}', plot=True, subplot=(2,1,1))
# 使用subplot参数将幅频和相频图分别画在指定位置
plt.subplot(2, 1, 1)
plt.title('比例环节伯德图 - 幅频特性')
plt.grid(which='both', linestyle='--', linewidth=0.5, alpha=0.7)
plt.legend()
plt.subplot(2, 1, 2)
# 比例环节相位为0,为了图示清晰,我们手动设置y轴范围
plt.ylim(-5, 5)
plt.title('比例环节伯德图 - 相频特性')
plt.grid(which='both', linestyle='--', linewidth=0.5, alpha=0.7)
plt.tight_layout()
plt.show()
```
运行这段代码,你会看到三条水平线。当K=0.1(即1/10)时,增益为20*log10(0.1) = -20 dB,意味着系统将该频率信号的幅度衰减到原来的十分之一。K=1时增益为0 dB,K=10时增益为+20 dB。
**积分与微分环节 (G(s) = 1/s 与 G(s) = s)**
积分和微分环节互为“镜像”。积分环节的幅频特性是一条斜率为-20 dB/decade的直线,相频恒为-90度。微分环节则相反,斜率为+20 dB/decade,相位恒为+90度。
```python
# 对比积分与微分环节
sys_integrator = ct.tf([1], [1, 0]) # 1/s
sys_differentiator = ct.tf([1, 0], [1]) # s
omega = np.logspace(-1, 2, 500) # 频率范围从0.1 rad/s 到 100 rad/s
plt.figure(figsize=(12, 8))
# 绘制积分环节
mag_i, phase_i, omega_i = ct.bode(sys_integrator, omega=omega, plot=False)
plt.subplot(2, 2, 1)
plt.semilogx(omega_i, 20*np.log10(mag_i), 'b-', linewidth=2, label='积分 1/s')
plt.grid(which='both', alpha=0.5)
plt.ylabel('幅值 (dB)')
plt.title('积分与微分环节幅频对比')
plt.legend()
plt.subplot(2, 2, 3)
plt.semilogx(omega_i, phase_i, 'b-', linewidth=2)
plt.grid(which='both', alpha=0.5)
plt.ylabel('相位 (度)')
plt.xlabel('频率 (rad/s)')
plt.ylim(-100, 100)
# 绘制微分环节
mag_d, phase_d, omega_d = ct.bode(sys_differentiator, omega=omega, plot=False)
plt.subplot(2, 2, 2)
plt.semilogx(omega_d, 20*np.log10(mag_d), 'r--', linewidth=2, label='微分 s')
plt.grid(which='both', alpha=0.5)
plt.title('微分环节')
plt.legend()
plt.subplot(2, 2, 4)
plt.semilogx(omega_d, phase_d, 'r--', linewidth=2)
plt.grid(which='both', alpha=0.5)
plt.xlabel('频率 (rad/s)')
plt.ylim(-100, 100)
plt.tight_layout()
plt.show()
```
这里我们使用了 `ct.bode(..., plot=False)` 来获取计算数据,然后用 `plt.semilogx` 手动绘图。这种方式给予了我们更大的定制自由度,比如可以轻松地将两个系统的曲线放在同一坐标系中进行对比。从图中可以清晰看到,两条幅频曲线关于0dB线对称,两条相频曲线关于0度线对称。
### 2.2 惯性环节与一阶微分:转折频率的魅力
惯性环节 `G(s) = 1/(Ts+1)` 和一阶微分环节 `G(s) = Ts+1` 是另一对常见的对偶环节,它们的伯德图在转折频率 `w_c = 1/T` 处发生显著变化。
**惯性环节 (低通滤波器)**
惯性环节的幅频特性在低频段近似为0dB的水平线,在高频段以-20 dB/decade的斜率下降。相位从0度开始下降,在转折频率处为-45度,最终趋近于-90度。
```python
# 分析不同时间常数T对惯性环节伯德图的影响
T_values = [0.1, 1, 10] # 时间常数
colors = ['blue', 'green', 'red']
plt.figure(figsize=(14, 5))
for T, color in zip(T_values, colors):
sys = ct.tf([1], [T, 1]) # 1/(T*s + 1)
w_c = 1/T # 计算转折频率
mag, phase, omega = ct.bode(sys, omega=np.logspace(-2, 2, 1000), plot=False)
plt.subplot(1, 2, 1)
plt.semilogx(omega, 20*np.log10(mag), color=color, linewidth=1.5,
label=f'T={T} (ω_c={w_c:.1f} rad/s)')
# 标记转折频率点
plt.semilogx([w_c, w_c], [-40, 5], color=color, linestyle=':', alpha=0.7)
plt.subplot(1, 2, 2)
plt.semilogx(omega, phase, color=color, linewidth=1.5, label=f'T={T}')
plt.subplot(1, 2, 1)
plt.grid(which='both', linestyle='--', alpha=0.7)
plt.ylabel('幅值 (dB)')
plt.xlabel('频率 (rad/s)')
plt.title('惯性环节幅频特性 (不同时间常数)')
plt.legend()
plt.ylim(-40, 5)
plt.subplot(1, 2, 2)
plt.grid(which='both', linestyle='--', alpha=0.7)
plt.ylabel('相位 (度)')
plt.xlabel('频率 (rad/s)')
plt.title('惯性环节相频特性')
plt.legend()
plt.ylim(-90, 5)
plt.tight_layout()
plt.show()
```
观察图表,你会发现时间常数 `T` 直接决定了转折频率的位置。`T` 越大(系统惯性越大),转折频率 `w_c` 越低,意味着系统对更低频率的信号就开始产生衰减和相位滞后,带宽越窄。这是一个非常关键的参数,在滤波器设计和系统响应速度调整中至关重要。
**一阶微分环节**
一阶微分环节 `Ts+1` 的伯德图与惯性环节关于频率轴大致对称。其幅频特性在低频段为0dB,高频段以+20 dB/decade上升;相位从0度开始上升,在转折频率处为+45度,最终趋近+90度。它在控制系统中常作为相位超前补偿器使用。
### 2.3 振荡环节:阻尼比与谐振峰值
振荡环节 `G(s) = ω_n^2 / (s^2 + 2ζω_n s + ω_n^2)` 是二阶系统的代表,其行为由无阻尼自然频率 `ω_n` 和阻尼比 `ζ` 共同决定。它的伯德图最为复杂,也最能体现系统动态特性。
```python
# 探究阻尼比ζ对振荡环节伯德图的影响,固定ω_n
omega_n = 10 # 无阻尼自然频率设为 10 rad/s
zeta_values = [0.1, 0.4, 0.707, 1.2] # 欠阻尼、欠阻尼、临界阻尼、过阻尼
labels = ['ζ=0.1 (欠阻尼)', 'ζ=0.4 (欠阻尼)', 'ζ=0.707 (临界阻尼)', 'ζ=1.2 (过阻尼)']
plt.figure(figsize=(15, 10))
for i, zeta in enumerate(zeta_values):
# 构造振荡环节传递函数
num = [omega_n**2]
den = [1, 2*zeta*omega_n, omega_n**2]
sys = ct.tf(num, den)
mag, phase, omega = ct.bode(sys, omega=np.logspace(-1, 2, 1000), plot=False)
# 绘制幅频图
plt.subplot(2, 2, 1)
plt.semilogx(omega, 20*np.log10(mag), linewidth=1.5, label=labels[i])
# 绘制相频图
plt.subplot(2, 2, 2)
plt.semilogx(omega, phase, linewidth=1.5, label=labels[i])
# 计算并标记谐振峰值(仅当ζ<0.707时存在)
if zeta < np.sqrt(2)/2: # 约0.707
Mr = 1 / (2*zeta*np.sqrt(1-zeta**2)) # 谐振峰值
wr = omega_n * np.sqrt(1 - 2*zeta**2) # 谐振频率
plt.subplot(2, 2, 1)
plt.plot(wr, 20*np.log10(Mr), 'o', markersize=8)
plt.annotate(f'Mr={Mr:.2f}', xy=(wr, 20*np.log10(Mr)), xytext=(10, 10),
textcoords='offset points', fontsize=9)
# 幅频图美化
plt.subplot(2, 2, 1)
plt.grid(which='both', linestyle='--', alpha=0.7)
plt.ylabel('幅值 (dB)')
plt.title(f'振荡环节幅频特性 (ω_n={omega_n} rad/s)')
plt.legend(loc='upper right')
plt.axvline(x=omega_n, color='k', linestyle=':', alpha=0.5, label=f'ω_n={omega_n}')
plt.legend()
# 相频图美化
plt.subplot(2, 2, 2)
plt.grid(which='both', linestyle='--', alpha=0.7)
plt.ylabel('相位 (度)')
plt.title(f'振荡环节相频特性 (ω_n={omega_n} rad/s)')
plt.legend()
plt.axvline(x=omega_n, color='k', linestyle=':', alpha=0.5)
# 为了更深入理解,我们可以单独分析ζ=0.2时的频率响应
plt.subplot(2, 2, 3)
zeta_detail = 0.2
sys_detail = ct.tf([omega_n**2], [1, 2*zeta_detail*omega_n, omega_n**2])
mag_d, phase_d, omega_d = ct.bode(sys_detail, omega=np.logspace(-1, 2, 1000), plot=False)
plt.semilogx(omega_d, 20*np.log10(mag_d), 'b-', linewidth=2, label=f'ζ={zeta_detail}')
plt.grid(which='both', linestyle='--', alpha=0.7)
plt.ylabel('幅值 (dB)')
plt.xlabel('频率 (rad/s)')
plt.title('欠阻尼系统 (ζ=0.2) 的谐振现象')
plt.legend()
# 添加一个奈奎斯特图作为补充视角
plt.subplot(2, 2, 4)
ct.nyquist_plot(sys_detail, omega=np.logspace(-1, 2, 1000), label=f'ζ={zeta_detail}')
plt.grid(True, alpha=0.5)
plt.title('对应振荡环节的奈奎斯特图')
plt.legend()
plt.tight_layout()
plt.show()
```
这段代码生成的图表信息量很大。我们可以清晰地观察到:
1. **阻尼比 `ζ` 对谐振峰值的影响**:当 `ζ < 0.707` 时,幅频曲线会出现一个高于0dB的峰值(谐振峰值 `Mr`),`ζ` 越小,峰值越高、越尖锐,系统在谐振频率 `ω_r` 附近的响应会被显著放大。这对机械结构避振、电路设计避免共振至关重要。
2. **相位变化**:无论 `ζ` 为何值,当频率经过 `ω_n` 时,相位都会经历大约-180度的快速下降。`ζ` 越小,相位变化曲线在 `ω_n` 附近越陡峭。
3. **`ω_n` 的意义**:`ω_n` 大致标定了系统频率响应的“中心”位置。幅频曲线的转折区域和相位曲线的快速下降区域都围绕 `ω_n` 展开。
> 提示:在实际工程中,我们常常希望系统具有一定的阻尼(如 `ζ` 在0.5~0.8之间),以兼顾响应速度和稳定性,避免过大的谐振峰值和超调。
## 3. 高级技巧:定制化绘图与系统分析
掌握了基本绘制方法后,我们可以让伯德图变得更专业、更贴合分析需求。`control` 库的 `bode_plot` 函数和 Matplotlib 的丰富接口提供了强大的定制能力。
### 3.1 多系统对比与自定义样式
在分析控制器效果或比较不同设计方案时,将多个系统的伯德图绘制在同一坐标系中非常有效。
```python
# 比较一个原始系统和一个加入PD控制器后的系统
s = ct.tf('s')
# 原始振荡环节系统
plant = 100 / (s**2 + 5*s + 100)
# 设计一个PD控制器: Kp + Kd*s
Kp, Kd = 2, 0.5
controller = Kp + Kd * s
# 闭环系统
sys_open = plant
sys_closed = ct.feedback(controller * plant, 1)
# 自定义绘图
plt.figure(figsize=(12, 8))
# 使用bode_plot并指定返回幅值相位数据,以便后续处理
mag_open, phase_open, omega = ct.bode(sys_open, plot=False)
mag_closed, phase_closed, _ = ct.bode(sys_closed, omega=omega, plot=False)
# 幅频图
ax1 = plt.subplot(2, 1, 1)
plt.semilogx(omega, 20*np.log10(mag_open), 'b-', linewidth=2.5, label='开环系统')
plt.semilogx(omega, 20*np.log10(mag_closed), 'r--', linewidth=2.5, label='带PD控制闭环系统')
plt.grid(which='both', linestyle=':', alpha=0.8)
plt.ylabel('幅值 (dB)', fontsize=12)
plt.title('开环 vs. 闭环系统伯德图对比', fontsize=14, fontweight='bold')
plt.legend(fontsize=11)
plt.axhline(y=0, color='k', linestyle='-', linewidth=0.5) # 0dB线
# 设置更精细的网格和刻度
ax1.set_axisbelow(True)
ax1.minorticks_on()
ax1.grid(which='minor', alpha=0.2)
# 相频图
ax2 = plt.subplot(2, 1, 2)
plt.semilogx(omega, phase_open, 'b-', linewidth=2.5, label='开环系统')
plt.semilogx(omega, phase_closed, 'r--', linewidth=2.5, label='带PD控制闭环系统')
plt.grid(which='both', linestyle=':', alpha=0.8)
plt.ylabel('相位 (度)', fontsize=12)
plt.xlabel('频率 (rad/s)', fontsize=12)
plt.legend(fontsize=11)
plt.axhline(y=-180, color='grey', linestyle='-', linewidth=0.5, alpha=0.7) # -180度线
ax2.set_axisbelow(True)
ax2.minorticks_on()
ax2.grid(which='minor', alpha=0.2)
plt.tight_layout()
plt.show()
```
通过对比,可以直观地看到PD控制器如何提高了系统在中高频段的增益(幅频曲线上翘)并提供了相位超前(相频曲线上移),这通常有助于提高系统的响应速度和稳定性裕度。
### 3.2 关键性能指标提取
伯德图不仅用于可视化,还能定量提取系统的稳定裕度等关键指标。`control` 库提供了便捷的函数。
```python
# 计算系统的增益裕度和相位裕度
gm, pm, wg, wp = ct.margin(sys_open)
print("=== 开环系统稳定裕度 ===")
print(f"增益裕度 Gm: {gm:.2f} (绝对比值) 或 {20*np.log10(gm):.2f} dB")
print(f"相位裕度 Pm: {pm:.2f} 度")
print(f"增益穿越频率 ω_gc: {wg:.2f} rad/s")
print(f"相位穿越频率 ω_pc: {wp:.2f} rad/s")
# 在伯德图上标注这些点
plt.figure(figsize=(10, 8))
mag, phase, omega = ct.bode(sys_open, omega=np.logspace(-1, 2, 1000), plot=False, dB=True)
plt.subplot(2, 1, 1)
plt.semilogx(omega, mag, 'b-', linewidth=2)
plt.grid(which='both', alpha=0.5)
plt.ylabel('幅值 (dB)')
plt.title('伯德图与稳定裕度标注')
# 标注增益穿越频率 (幅值曲线穿越0dB的点)
plt.axvline(x=wp, color='r', linestyle='--', alpha=0.7, label=f'ω_gc={wp:.2f}')
plt.plot(wp, 0, 'ro', markersize=8) # 0dB点
plt.legend()
plt.subplot(2, 1, 2)
plt.semilogx(omega, phase, 'b-', linewidth=2)
plt.grid(which='both', alpha=0.5)
plt.ylabel('相位 (度)')
plt.xlabel('频率 (rad/s)')
# 标注相位穿越频率 (相位曲线穿越-180度的点)
plt.axvline(x=wg, color='g', linestyle='--', alpha=0.7, label=f'ω_pc={wg:.2f}')
plt.axhline(y=-180, color='grey', linestyle='-', alpha=0.5)
plt.plot(wg, -180, 'go', markersize=8) # -180度点
plt.legend()
plt.tight_layout()
plt.show()
```
`ct.margin()` 函数一次性返回了所有关键信息。**相位裕度 (Pm)** 和 **增益裕度 (Gm)** 是衡量系统相对稳定性的核心指标。通常,我们希望相位裕度在30度到60度之间,增益裕度大于6dB,这样的系统既有较好的动态性能,又有足够的鲁棒性。
## 4. 常见问题排查与可视化优化
在实际编码过程中,你可能会遇到一些报错或得到不理想的图表。这里分享几个我踩过坑后总结的要点。
### 4.1 常见报错与解决思路
1. **`AttributeError: module 'control' has no attribute 'bode'`**
* **原因**:可能是 `control` 库版本问题或导入方式有误。较新版本的 `control` 中,`bode` 是一个函数,而 `bode_plot` 是用于绘图的函数。
* **解决**:确保安装的是最新版 (`pip install -U control`)。使用 `import control as ct` 后,调用 `ct.bode()` 或 `ct.bode_plot()`。
2. **伯德图曲线异常(如出现垂直直线、数值溢出)**
* **原因**:最常见的原因是频率向量 `omega` 包含了0(对数坐标无法处理0),或者系统在某个频率点存在奇异性(如极点位于虚轴上)。
* **解决**:使用 `np.logspace(start, stop, num)` 生成频率范围时,确保 `start` 是一个小的正数(如 `-2` 代表 `10^{-2}=0.01`),避免0。对于含有积分环节 `1/s` 的系统,其在 `ω=0` 处的增益为无穷大,绘图时会自动处理或跳过,但最好明确指定一个合理的起始频率。
```python
# 良好的频率范围设置示例
omega_safe = np.logspace(-2, 3, 500) # 从0.01 rad/s 到 1000 rad/s
mag, phase, omega = ct.bode(sys, omega=omega_safe, plot=False)
```
3. **`ValueError: The input is of invalid format.` (在创建传递函数时)**
* **原因**:`ct.tf(num, den)` 的参数 `num` 和 `den` 必须是列表或数组,且代表的是多项式系数,按降幂排列。例如,`s^2 + 3s + 2` 应表示为 `[1, 3, 2]`。
* **解决**:仔细检查分子分母系数列表。对于常数项K,分子是 `[K]`,分母是 `[1]`。可以使用 `print(ct.tf([1, 2], [1, 3, 2]))` 来查看创建的传递函数是否正确。
### 4.2 图表美化与导出
一份专业的报告需要清晰的图表。以下是一些提升伯德图可读性和美观性的技巧:
```python
# 创建一个出版级质量的伯德图
sys_example = ct.tf([1], [1, 1, 1]) # 一个简单的二阶系统
plt.figure(figsize=(10, 8), dpi=150) # 设置高分辨率
# 使用bode_plot并获取坐标轴对象进行精细控制
mag, phase, omega, fig, axes = ct.bode_plot(sys_example, omega=np.logspace(-1, 2, 1000),
dB=True, Hz=False, deg=True,
plot=True, grid=True, margins=True)
ax_mag, ax_phase = axes # 解包幅频和相频图的坐标轴对象
# 1. 设置标题和标签字体
ax_mag.set_title('幅频特性', fontsize=14, fontweight='bold')
ax_phase.set_title('相频特性', fontsize=14, fontweight='bold')
ax_phase.set_xlabel('频率 [rad/s]', fontsize=12)
ax_mag.set_ylabel('幅值 [dB]', fontsize=12)
ax_phase.set_ylabel('相位 [度]', fontsize=12)
# 2. 美化网格线
ax_mag.grid(which='major', linestyle='-', linewidth=0.7, alpha=0.8)
ax_mag.grid(which='minor', linestyle=':', linewidth=0.5, alpha=0.4)
ax_phase.grid(which='major', linestyle='-', linewidth=0.7, alpha=0.8)
ax_phase.grid(which='minor', linestyle=':', linewidth=0.5, alpha=0.4)
# 3. 设置坐标轴刻度字体
for ax in [ax_mag, ax_phase]:
ax.tick_params(axis='both', which='major', labelsize=10)
# 4. 调整曲线样式 (bode_plot内部绘制,可通过获取lines对象修改)
lines_mag = ax_mag.get_lines()
lines_phase = ax_phase.get_lines()
if lines_mag:
lines_mag[0].set_linewidth(2)
lines_mag[0].set_color('darkblue')
if lines_phase:
lines_phase[0].set_linewidth(2)
lines_phase[0].set_color('darkred')
# 5. 添加自定义标注
# 例如,标注-3dB带宽点
mag_linear = 10**(mag/20) # 将dB转换回线性值
idx_3db = np.argmin(np.abs(mag_linear - 1/np.sqrt(2))) # 找到增益为1/sqrt(2)的点
freq_3db = omega[idx_3db]
ax_mag.axvline(x=freq_3db, color='orange', linestyle='--', alpha=0.7)
ax_mag.annotate(f'-3dB BW\n{