4、Python环境与仿真框架搭建:安装必要的Python库、搭建基于Python的无人机仿真框架

好,咱们进入第四讲。说实话,很多同学觉得调LQR控制器是核心,环境搭建不过是跑几个pip命令,没什么技术含量。我以前也这么想,直到有一次在项目交付前三天,发现仿真环境和实飞环境对不上,差点把整个项目节奏打乱。嗯,从那以后,我每次都会花半天时间,认认真真把仿真框架搭好。

这一章,咱们就聊聊怎么搭一个靠谱的Python仿真环境。说白了,就是让你的代码跑得起来、跑得对、跑得稳。

4.1 安装必要的Python库

我个人习惯用Anaconda来管理Python环境,这样不同项目之间不会打架。你如果已经装了Python 3.8以上版本,直接用pip也行。

先列一下咱们需要的库:

库名 版本建议 用途
numpy ≥1.21 矩阵运算、数值计算
scipy ≥1.7 求解微分方程、插值、优化
matplotlib ≥3.4 数据可视化、轨迹绘制
control ≥0.9 LQR求解、系统分析

安装命令很简单,打开终端,一行搞定:

pip install numpy scipy matplotlib control
小提示: 如果你用的是国内网络,建议加个镜像源,比如 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy scipy matplotlib control,速度会快很多。

安装完成后,可以跑个简单的测试脚本,看看库能不能正常导入:

import numpy as np
import scipy as sp
import matplotlib.pyplot as plt
import control as ct

print("numpy版本:", np.__version__)
print("scipy版本:", sp.__version__)
print("control版本:", ct.__version__)
print("所有库导入成功!")

如果没报错,恭喜你,环境基础打好了。

4.2 仿真框架的整体结构

搭建仿真框架,说白了就是搭一个「虚拟试飞场」。我一般把它分成三个模块:

  • 无人机模型模块:描述四旋翼的动力学和运动学
  • 控制器模块:实现LQR控制律
  • 仿真主循环模块:驱动整个仿真过程,记录数据

你想想看,这三个模块各司其职,就像工厂里的流水线。模型模块负责「被控对象」,控制器模块负责「决策」,主循环负责「调度」。

下面这张图,是我自己画的一个框架流程图,你可以参考一下:

无人机LQR轨迹跟踪仿真框架 无人机模型模块 动力学 + 运动学 状态更新: x(k+1) = f(x(k), u(k)) LQR控制器模块 计算控制量: u = -K * (x - x_ref) 增益矩阵K由Riccati方程求解 仿真主循环模块 时间步进: t = 0, dt, 2*dt, ... 数据记录 + 可视化 状态x(k) 控制量u(k) 控制量u(k) 状态x(k) + 参考轨迹 参考轨迹输入 x_ref(t), y_ref(t), z_ref(t)

4.3 搭建无人机模型模块

四旋翼的动力学模型,说白了就是牛顿第二定律加上欧拉方程。我这里用一个简化模型,只考虑位置和速度,忽略姿态动力学——因为咱们先聚焦在LQR的调参上。

模型的状态量我取为:

状态向量 x = [x, y, z, vx, vy, vz]^T
控制输入 u = [ax, ay, az]^T  (加速度)

离散化后的状态方程:

x(k+1) = A * x(k) + B * u(k)

其中:

A = [[1, 0, 0, dt, 0,  0],
     [0, 1, 0, 0,  dt, 0],
     [0, 0, 1, 0,  0,  dt],
     [0, 0, 0, 1,  0,  0],
     [0, 0, 0, 0,  1,  0],
     [0, 0, 0, 0,  0,  1]]

B = [[0.5*dt^2, 0,        0],
     [0,        0.5*dt^2, 0],
     [0,        0,        0.5*dt^2],
     [dt,       0,        0],
     [0,        dt,       0],
     [0,        0,        dt]]

代码实现如下:

import numpy as np

class QuadrotorModel:
    def __init__(self, dt=0.01):
        self.dt = dt
        self.A = np.array([
            [1, 0, 0, dt, 0,  0],
            [0, 1, 0, 0,  dt, 0],
            [0, 0, 1, 0,  0,  dt],
            [0, 0, 0, 1,  0,  0],
            [0, 0, 0, 0,  1,  0],
            [0, 0, 0, 0,  0,  1]
        ])
        self.B = np.array([
            [0.5*dt**2, 0,         0],
            [0,         0.5*dt**2, 0],
            [0,         0,         0.5*dt**2],
            [dt,        0,         0],
            [0,         dt,        0],
            [0,         0,         dt]
        ])
    
    def step(self, state, control):
        """执行一步仿真"""
        return self.A @ state + self.B @ control
关键点: 这里的dt是仿真步长,我一般取0.01秒(100Hz)。步长太大,仿真精度不够;步长太小,计算量太大。100Hz是个不错的折中。

4.4 搭建LQR控制器模块

LQR控制器的核心,就是求解Riccati方程得到增益矩阵K。control库提供了现成的函数,咱们直接调用就行。

import control as ct

class LQRController:
    def __init__(self, A, B, Q, R):
        self.A = A
        self.B = B
        self.Q = Q
        self.R = R
        # 求解LQR增益
        K, S, E = ct.lqr(A, B, Q, R)
        self.K = np.array(K)
        print("LQR增益矩阵K已求解完成,形状:", self.K.shape)
    
    def compute_control(self, state, ref_state):
        """计算控制量 u = -K * (state - ref_state)"""
        error = state - ref_state
        u = -self.K @ error
        return u
注意: ct.lqr函数默认求解连续系统的LQR增益。如果你的模型是离散的,需要用ct.dlqr。我这里的模型是离散的,所以应该用ct.dlqr。不过control库的lqr函数会自动判断,如果你传入的是离散系统矩阵,它会自动调用dlqr。但为了保险起见,我建议你显式调用ct.dlqr。

4.5 搭建仿真主循环

主循环就是让时间往前走,每一步都调用模型和控制器,记录数据。我一般这样写:

def run_simulation(model, controller, initial_state, ref_trajectory, steps):
    """
    运行仿真
    model: 无人机模型
    controller: LQR控制器
    initial_state: 初始状态
    ref_trajectory: 参考轨迹,形状为 (steps, 6)
    steps: 仿真步数
    """
    state = initial_state.copy()
    states = [state.copy()]
    controls = []
    
    for k in range(steps):
        ref = ref_trajectory[k]
        u = controller.compute_control(state, ref)
        state = model.step(state, u)
        
        states.append(state.copy())
        controls.append(u.copy())
    
    return np.array(states), np.array(controls)

你看,这个主循环其实很简单。但我在项目中遇到过一个问题:如果参考轨迹突然变化太大,控制器会输出很大的控制量,导致仿真发散。所以后来我加了一个限幅:

def compute_control_safe(self, state, ref_state, u_max=10.0):
    error = state - ref_state
    u = -self.K @ error
    # 限幅
    u = np.clip(u, -u_max, u_max)
    return u
经验之谈: 限幅值u_max怎么选?我一般先设一个比较大的值(比如20),跑一遍仿真看看控制量的范围,再根据实际情况调整。太小了会限制控制性能,太大了可能不安全。

4.6 可视化与调试

仿真跑完了,得看看效果。我习惯用matplotlib画三张图:

  1. 三维轨迹图:看无人机实际轨迹和参考轨迹的贴合程度
  2. 位置误差图:看x、y、z三个方向的误差随时间变化
  3. 控制量图:看三个轴的控制加速度是否合理

代码示例:

import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

def plot_results(states, ref_trajectory, controls):
    fig = plt.figure(figsize=(15, 5))
    
    # 三维轨迹
    ax1 = fig.add_subplot(131, projection='3d')
    ax1.plot(states[:, 0], states[:, 1], states[:, 2], 'b-', label='实际轨迹')
    ax1.plot(ref_trajectory[:, 0], ref_trajectory[:, 1], ref_trajectory[:, 2], 'r--', label='参考轨迹')
    ax1.set_xlabel('X (m)')
    ax1.set_ylabel('Y (m)')
    ax1.set_zlabel('Z (m)')
    ax1.legend()
    
    # 位置误差
    ax2 = fig.add_subplot(132)
    error = states[:-1] - ref_trajectory
    ax2.plot(error[:, 0], label='X误差')
    ax2.plot(error[:, 1], label='Y误差')
    ax2.plot(error[:, 2], label='Z误差')
    ax2.set_xlabel('时间步')
    ax2.set_ylabel('误差 (m)')
    ax2.legend()
    ax2.grid(True)
    
    # 控制量
    ax3 = fig.add_subplot(133)
    ax3.plot(controls[:, 0], label='ax')
    ax3.plot(controls[:, 1], label='ay')
    ax3.plot(controls[:, 2], label='az')
    ax3.set_xlabel('时间步')
    ax3.set_ylabel('加速度 (m/s²)')
    ax3.legend()
    ax3.grid(True)
    
    plt.tight_layout()
    plt.show()

4.7 完整仿真示例

把上面所有模块组合起来,跑一个完整的仿真:

# 参数设置
dt = 0.01
steps = 1000  # 仿真10秒

# 初始化模型和控制器
model = QuadrotorModel(dt=dt)
Q = np.diag([10, 10, 10, 1, 1, 1])  # 状态权重
R = np.diag([1, 1, 1])              # 控制权重
controller = LQRController(model.A, model.B, Q, R)

# 生成参考轨迹:螺旋上升
t = np.linspace(0, steps*dt, steps)
ref_trajectory = np.zeros((steps, 6))
ref_trajectory[:, 0] = 5 * np.cos(0.5 * t)  # x
ref_trajectory[:, 1] = 5 * np.sin(0.5 * t)  # y
ref_trajectory[:, 2] = 0.5 * t               # z

# 初始状态
initial_state = np.array([5.0, 0.0, 0.0, 0.0, 0.0, 0.0])

# 运行仿真
states, controls = run_simulation(model, controller, initial_state, ref_trajectory, steps)

# 可视化
plot_results(states, ref_trajectory, controls)
总结一下: 这一章咱们把仿真框架搭起来了。你想想看,有了这个框架,后面调LQR参数就方便多了——改改Q和R矩阵,跑一遍仿真,看看效果,再改再跑。这就是「调参闭环」。

嗯,框架搭好了,咱们就可以开始真正的调参之旅了。下一章我会聊聊Q和R矩阵的物理意义,以及怎么根据实际需求来设置它们。不过那是后话了,先把今天的代码跑通再说。


专注资料整理