基于Python开发fhan仿真工具

一、小程序功能设计

1、经典非线性跟踪微分器回顾

(1)经典非线性跟踪微分器(韩京清形式)的数学表达式如下所示

(2)非线性函数fhan称为最速控制函数,其数学定义为

2、CSV信号配置

(1)小程序支持导入CSV文件,用户可以选择直接配置CSV数据的原始采样周期,如果CSV数据中有时间轴,也可直接选择时间列的数据作为横轴,然后选择输入跟踪微分器的信号列。

(2)功能定义:

①点击“导入CSV”,选择需要导入的文件,即可将CSV数据导入小程序。

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

③跟踪微分器同时也需要输入微分信号列,但大多数CSV数据本身不会有微分信号列,所以小程序支持自动根据原始信号列计算微分信号列。

3、仿真曲线绘制

(1)在参数配置区可配置参数模型(调度周期Ts不要大于控制步长h),然后点击“添加本组”,即可加入一组参数(支持添加多组参数);选中列表中的一组参数,点击“删除选中”,即可将参数组移除。

(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

# ========= 韩京清标准fhan函数(含h0前置滤波) =============
def fsg(x, d):
    if abs(x) <= d:
        return x / (2 * d) + 0.5
    return 1 if x > 0 else 0

def fhan(v, x1, x2, r, h):
    d = r * h ** 2
    a0 = h * x2
    y = x1 - v + a0
    a1 = np.sqrt(d * (np.abs(y) + 8 * d))
    a2 = a0 + np.sign(y) * (a1 - d) / 2.0
    a = a2 * (1 - fsg(y, d)) + (x1 - v + a0) * fsg(y, d)
    u = -r * fsg(a, d) - r * (a / d) * (1 - fsg(a, d))
    return u

# ===================== GUI主程序 =====================
class FhanTool:
    def __init__(self, root):
        self.root = root
        self.root.title("FHAN仿真工具")
        self.root.geometry("1360x880")

        self.df = None
        self.csv_columns = []
        self.file_text = tk.StringVar(value="未加载CSV文件")
        # CSV原始硬件采样周期(仅当X轴选"样本序号"时使用)
        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信号配置(仅需1列原始信号,自动微分)")
        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)

        ttk.Label(csv_frame, text="x2微分来源:").grid(row=1, column=0, padx=10, pady=4)
        self.var_x2 = tk.StringVar()
        self.cbx_x2 = ttk.Combobox(csv_frame, textvariable=self.var_x2, state="readonly", width=22)
        self.cbx_x2.grid(row=1, column=1)

        # ========== FHAN参数配置 ==========
        param_frame = ttk.LabelFrame(root, text="FHAN参数组:r快速因子 / h控制步长 / h0前置滤波 / Ts调度周期(s)")
        param_frame.pack(fill="x", padx=8, pady=4)

        ttk.Label(param_frame, text="r:").grid(row=0, column=0, padx=4, pady=5)
        self.var_r_in = tk.StringVar(value="100")
        ttk.Entry(param_frame, textvariable=self.var_r_in, width=10).grid(row=0, column=1)

        ttk.Label(param_frame, text="h:").grid(row=0, column=2, padx=8)
        self.var_h_in = tk.StringVar(value="0.01")
        ttk.Entry(param_frame, textvariable=self.var_h_in, width=10).grid(row=0, column=3)

        ttk.Label(param_frame, text="h0:").grid(row=0, column=4, padx=8)
        self.var_h0_in = tk.StringVar(value="0.005")
        ttk.Entry(param_frame, textvariable=self.var_h0_in, width=10).grid(row=0, column=5)

        ttk.Label(param_frame, text="Ts调度周期(s):").grid(row=0, column=6, 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=7)

        ttk.Button(param_frame, text="添加本组", command=self.add_param).grid(row=0, column=8, padx=6)
        ttk.Button(param_frame, text="删除选中", command=self.del_param).grid(row=0, column=9, padx=6)
        ttk.Button(param_frame, text="清空全部", command=self.clear_param).grid(row=0, column=10, padx=4)

        self.param_tree = ttk.Treeview(param_frame, columns=("r", "h", "h0", "Ts"), show="headings", height=4)
        self.param_tree.heading("r", text="r快速因子")
        self.param_tree.heading("h", text="h步长")
        self.param_tree.heading("h0", text="h0滤波")
        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):
        # 选了真实时间列就禁用CSV采样周期输入(时间列自带时间戳)
        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])
            x2_list = ["自动微分(按本组Ts重采样后计算)", *self.csv_columns]
            self.cbx_x2["values"] = x2_list
            self.var_x2.set("自动微分(按本组Ts重采样后计算)")
            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:
            r = float(self.var_r_in.get())
            h = float(self.var_h_in.get())
            h0 = float(self.var_h0_in.get())
            ts = float(self.var_ts_in.get())
            if ts <= 0:
                messagebox.showerror("错误", "Ts必须大于0!")
                return
        except:
            messagebox.showerror("参数错误", "r/h/h0/Ts必须为数字!")
            return
        self.param_list.append((r, h, h0, ts))
        self.param_tree.insert("", "end", values=(r, h, h0, 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原始时间轴:选了时间列就用该列,否则用样本序号×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 draw_all(self):
        if self.df is None:
            messagebox.showwarning("提示", "请先导入CSV文件")
            return
        if len(self.param_list) == 0:
            messagebox.showwarning("提示", "至少添加一组FHAN参数")
            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)
        x2_sel = self.var_x2.get()

        self.ax.clear()
        # 画原始x1信号(用原始时间轴)
        self.ax.plot(t_orig, x1_orig, color="#1f77b4", linewidth=1.4, label="x1原始信号")

        # 遍历每组参数:按本组Ts重采样 → 迭代运行跟踪微分器
        for idx, (r, h, h0, ts) in enumerate(self.param_list):
            # 目标时间轴:从原始起点到终点,步长ts
            t_new = np.arange(t_orig[0], t_orig[-1] + ts * 0.5, ts)
            # 线性插值重采样得到目标输入v序列
            v_seq = np.interp(t_new, t_orig, x1_orig)

            # -------- 前置滤波 h0,对输入v做,不是塞进fhan --------
            alpha = ts / (h0 + ts)
            v_filt = np.zeros_like(v_seq)
            v_filt[0] = v_seq[0]
            for i in range(1, len(v_seq)):
                v_filt[i] = alpha * v_seq[i] + (1 - alpha) * v_filt[i - 1]

            # -------- 离散跟踪微分器迭代,初始化状态 --------
            x1 = v_filt[0]
            x2 = 0.0
            out_x1 = np.zeros_like(v_filt)
            out_x2 = np.zeros_like(v_filt)

            for i, v in enumerate(v_filt):
                fh = fhan(v, x1, x2, r, h)
                x1 = x1 + ts * x2
                x2 = x2 + ts * fh
                out_x1[i] = x1
                out_x2[i] = x2

            c = self.line_colors[idx % len(self.line_colors)]
            # 绘制跟踪输出 out_x1,微分输出 out_x2
            self.ax.plot(t_new, out_x1, color=c, linewidth=1.6, label=f"FHAN跟踪输出 r={r} h={h} h0={h0} 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("FHAN多参数同轴仿真(按Ts重采样)")
        self.canvas.draw()

if __name__ == "__main__":
    root = tk.Tk()
    app = FhanTool(root)
    root.mainloop()

3、可执行程序生成及注意事项

(1)在程序源码文件fhan_analyze.py所在目录下打开终端,输入命令“pyinstaller -w -F fhan_analyze.py”,即可生成可执行文件fhan_analyze.exe。

(2)当前小程序仅支持曲线图像生成,不支持曲线图像保存,如有需求,可基于以上程序源码二次开发。

(3)传统控制算法往往关注很小的时间单位(毫秒级别),如果希望仿真生成的图像具有参考意义,建议导入CSV数据的时间范围不要过长(当然,也可基于以上程序源码二次开发,增加仿真时间范围配置项)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zevalin爱灰灰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值