一、小程序功能设计
1、临界二阶阻尼传递函数模型回顾
(1)二阶系统的结构:

(2)不同阻尼的二阶系统响应:



(3)根据以上的内容回顾,临界阻尼二阶系统有如下特点:

(4)基于临界阻尼二阶系统的特点,可以设计一个响应调节器,对某些变化剧烈的信号进行响应调节,使其变化平滑。不过请求信号往往是通过控制器发出的,而要想算法在控制器中运行,它必须是离散的,因此对于连续的响应调节器,要想应用于软件控制中,必须将其离散化,离散化后得到的状态空间表达式如下所示。

2、CSV信号配置
(1)小程序支持导入CSV文件,用户可以选择直接配置CSV数据的原始采样周期,如果CSV数据中有时间轴,也可直接选择时间列的数据作为横轴,然后选择输入跟踪微分器的信号列。
(2)功能定义:
①点击“导入CSV”,选择需要导入的文件,即可将CSV数据导入小程序。

②在右侧下拉框可以配置横轴的时间列和纵轴的原始信号列,如果选择使用CSV的原始采样周期,则不配置横轴的时间列。

3、仿真曲线绘制
(1)在参数配置区可配置参数模型(自然频率不要太大,需要小于调度频率,否则系统会振荡),然后点击“添加本组”,即可加入一组参数(支持添加多组参数);选中列表中的一组参数,点击“删除选中”,即可将参数组移除。

(2)完成参数配置后,点击“批量绘制对比曲线”,小程序会把原始数据、经过各组参数模型处理过的平滑数据共轴展示。

二、小程序开发
1、开发环境
(1)需要确认安装的Python是否自带tkinter库(图形依赖),有则无需单独安装。
(2)安装pyinstaller库,该库主要用于将.py打包成可执行文件.exe,使小程序运行能够脱离Python环境。
(3)安装numpy库,该库用于支持复杂的科学计算。
2、程序源码
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import pandas as pd
import numpy as np
import matplotlib
matplotlib.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
matplotlib.rcParams['axes.unicode_minus'] = False
matplotlib.use("TkAgg")
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class CritDamp2ndSimGUI:
def __init__(self, root):
self.root = root
self.root.title("临界阻尼二阶系统仿真工具 ζ=1")
self.root.geometry("1360x880")
self.df = None
self.csv_columns = []
self.file_text = tk.StringVar(value="未加载CSV文件")
# CSV原始采样周期
self.var_csv_ts = tk.StringVar(value="0.001")
self.param_list = []
self.line_colors = ["#d62728", "#2ca02c", "#ff7f0e", "#9467bd", "#17becf", "#bcbd22"]
# ========== CSV信号配置区(带时间、信号下拉框) ==========
csv_frame = ttk.LabelFrame(root, text="CSV信号配置(输入临界阻尼二阶系统)")
csv_frame.pack(fill="x", padx=8, pady=4)
ttk.Button(csv_frame, text="导入CSV", command=self.load_csv).grid(row=0, column=0, padx=5, pady=6)
ttk.Label(csv_frame, textvariable=self.file_text).grid(row=0, column=1, padx=10)
ttk.Label(csv_frame, text="CSV原始采样周期(s):").grid(row=0, column=2, padx=10)
self.ent_csv_ts = ttk.Entry(csv_frame, textvariable=self.var_csv_ts, width=12)
self.ent_csv_ts.grid(row=0, column=3)
ttk.Label(csv_frame, text="X轴时间列:").grid(row=0, column=4, padx=10)
self.var_time = tk.StringVar()
self.cbx_time = ttk.Combobox(csv_frame, textvariable=self.var_time, state="readonly", width=14)
self.cbx_time.grid(row=0, column=5)
self.cbx_time.bind("<<ComboboxSelected>>", self._on_time_col_change)
ttk.Label(csv_frame, text="x1输入信号列:").grid(row=0, column=6, padx=10)
self.var_x1 = tk.StringVar()
self.cbx_x1 = ttk.Combobox(csv_frame, textvariable=self.var_x1, state="readonly", width=14)
self.cbx_x1.grid(row=0, column=7)
# ========== 参数配置区:wn自然频率 / Ts仿真周期 ==========
param_frame = ttk.LabelFrame(root, text="临界阻尼二阶参数组:wn自然频率(rad/s) / Ts调度周期(s) ζ固定=1")
param_frame.pack(fill="x", padx=8, pady=4)
ttk.Label(param_frame, text="wn:").grid(row=0, column=0, padx=4, pady=5)
self.var_wn_in = tk.StringVar(value="10.0")
ttk.Entry(param_frame, textvariable=self.var_wn_in, width=10).grid(row=0, column=1)
ttk.Label(param_frame, text="Ts调度周期(s):").grid(row=0, column=2, padx=8)
self.var_ts_in = tk.StringVar(value="0.001")
ttk.Entry(param_frame, textvariable=self.var_ts_in, width=10).grid(row=0, column=3)
ttk.Button(param_frame, text="添加本组", command=self.add_param).grid(row=0, column=4, padx=6)
ttk.Button(param_frame, text="删除选中", command=self.del_param).grid(row=0, column=5, padx=6)
ttk.Button(param_frame, text="清空全部", command=self.clear_param).grid(row=0, column=6, padx=4)
self.param_tree = ttk.Treeview(param_frame, columns=("wn", "Ts"), show="headings", height=4)
self.param_tree.heading("wn", text="wn自然频率(rad/s)")
self.param_tree.heading("Ts", text="Ts调度周期(s)")
self.param_tree.grid(row=1, column=0, columnspan=11, sticky="ew", pady=6)
param_frame.columnconfigure(10, weight=1)
# 绘图按钮
btn_frame = ttk.Frame(root)
btn_frame.pack(fill="x", padx=8, pady=3)
ttk.Button(btn_frame, text="批量绘制对比曲线", command=self.draw_all).pack()
# 绘图画布
plot_frame = ttk.LabelFrame(root, text="输入输出同轴对比曲线")
plot_frame.pack(fill="both", expand=True, padx=8, pady=6)
self.fig, self.ax = plt.subplots(figsize=(11, 6.8), dpi=96)
self.canvas = FigureCanvasTkAgg(self.fig, master=plot_frame)
self.canvas_widget = self.canvas.get_tk_widget()
self.canvas_widget.pack(fill="both", expand=True)
self.canvas.draw()
def _on_time_col_change(self, event=None):
# 选择样本序号时启用采样周期输入,选真实时间列则禁用
if self.var_time.get() == "样本序号":
self.ent_csv_ts.config(state="normal")
else:
self.ent_csv_ts.config(state="disabled")
def load_csv(self):
path = filedialog.askopenfilename(filetypes=[("CSV文件", "*.csv"), ("所有文件", "*.*")])
if not path:
return
try:
self.df = pd.read_csv(path)
self.csv_columns = list(self.df.columns)
time_list = ["样本序号"] + self.csv_columns
self.cbx_time["values"] = time_list
self.var_time.set("样本序号")
self.cbx_x1["values"] = self.csv_columns
if self.csv_columns:
self.var_x1.set(self.csv_columns[0])
self.ent_csv_ts.config(state="normal")
self.file_text.set(f"已加载:{path.split('/')[-1]}")
messagebox.showinfo("提示", "CSV加载完成,每组参数按各自Ts自动重采样仿真")
except Exception as e:
messagebox.showerror("读取失败", str(e))
self.df = None
self.csv_columns = []
def add_param(self):
try:
wn = float(self.var_wn_in.get())
ts = float(self.var_ts_in.get())
if wn <= 0 or ts <= 0:
messagebox.showerror("错误", "wn、Ts必须大于0!")
return
except:
messagebox.showerror("参数错误", "wn/Ts必须为数字!")
return
self.param_list.append((wn, ts))
self.param_tree.insert("", "end", values=(wn, ts))
def del_param(self):
sel = self.param_tree.selection()
if not sel:
messagebox.showwarning("提示", "请先选中一行参数")
return
idx = self.param_tree.index(sel[0])
del self.param_list[idx]
self.param_tree.delete(sel[0])
def clear_param(self):
self.param_list.clear()
for item in self.param_tree.get_children():
self.param_tree.delete(item)
def _get_orig_time(self):
"""获取CSV原始时间轴"""
time_sel = self.var_time.get()
if time_sel == "样本序号":
try:
t_csv = float(self.var_csv_ts.get())
except:
messagebox.showerror("错误", "CSV原始采样周期必须是数字!")
return None
if t_csv <= 0:
messagebox.showerror("错误", "CSV原始采样周期必须大于0!")
return None
return np.arange(len(self.df)) * t_csv
else:
return self.df[time_sel].to_numpy(dtype=float)
def sim_crit_damp_2nd(self, u_seq, Ts, wn):
"""
临界阻尼二阶系统 ζ=1
G(s) = wn² / (s² + 2*wn*s + wn²)
欧拉离散迭代仿真
"""
n = len(u_seq)
y = np.zeros(n)
dy = np.zeros(n)
for k in range(1, n):
u_k = u_seq[k]
# 二阶微分方程离散
ddy = (wn**2) * u_k - 2 * wn * dy[k-1] - (wn**2) * y[k-1]
dy[k] = dy[k-1] + ddy * Ts
y[k] = y[k-1] + dy[k-1] * Ts
return y
def draw_all(self):
if self.df is None:
messagebox.showwarning("提示", "请先导入CSV文件")
return
if len(self.param_list) == 0:
messagebox.showwarning("提示", "至少添加一组仿真参数")
return
t_orig = self._get_orig_time()
if t_orig is None:
return
x1_orig = self.df[self.var_x1.get()].to_numpy(dtype=float)
self.ax.clear()
# 绘制原始输入信号
self.ax.plot(t_orig, x1_orig, color="#1f77b4", linewidth=1.4, label="原始输入x1")
# 遍历多组参数仿真绘图
for idx, (wn, ts) in enumerate(self.param_list):
t_new = np.arange(t_orig[0], t_orig[-1] + ts * 0.5, ts)
v_seq = np.interp(t_new, t_orig, x1_orig)
y_out = self.sim_crit_damp_2nd(v_seq, ts, wn)
c = self.line_colors[idx % len(self.line_colors)]
self.ax.plot(t_new, y_out, color=c, linewidth=1.6, label=f"临界阻尼输出 wn={wn} Ts={ts}s")
self.ax.legend(loc="best")
self.ax.grid(True, alpha=0.3)
self.ax.set_xlabel("时间 (s)")
self.ax.set_ylabel("信号幅值")
self.ax.set_title("临界阻尼二阶系统(ζ=1)多参数同轴仿真对比")
self.canvas.draw()
if __name__ == "__main__":
root = tk.Tk()
app = CritDamp2ndSimGUI(root)
root.mainloop()
3、可执行程序生成及注意事项
(1)在程序源码文件crit_damp_2nd_analyze.py所在目录下打开终端,输入命令“pyinstaller -w -F crit_damp_2nd_analyze.py”,即可生成可执行文件crit_damp_2nd_analyze.exe。
(2)当前小程序仅支持曲线图像生成,不支持曲线图像保存,如有需求,可基于以上程序源码二次开发。
(3)传统控制算法往往关注很小的时间单位(毫秒级别),如果希望仿真生成的图像具有参考意义,建议导入CSV数据的时间范围不要过长(当然,也可基于以上程序源码二次开发,增加仿真时间范围配置项)。

235

被折叠的 条评论
为什么被折叠?



