这几年实验室里做无人船集群方向复现研究的同学不少,真正让人头疼的往往不是推导,而是论文里一句话带过的仿真参数。今天整理的这个“无人船编队在动态预设性能约束下的包容控制”复现过程,算是我踩过不少坑之后沉淀下来的完整记录。文章会从数学模型、约束设计、控制律推导一路讲到Matlab代码实现和调参避坑,适合正在做多智能体编队、预设性能控制或者无人船协同方向的研究生参考。
1. 整体设计与思路拆解
拿到这个课题以后,我第一件事就是拆标题:“无人船编队”、“动态预设性能约束”、“包容控制”这三个词分别对应了三个核心模块。先想清楚它们各自解决什么问题,再考虑怎么在Matlab里把它们拼成一个可运行的仿真。
无人船编队讲的是多船协同,核心是“编队怎么组织”。常见做法有领航者-跟随者、基于行为法、虚拟结构法,但这篇课题用的是包容控制思路。包容控制的通俗理解是:让一群跟随船落在由多条领航船围成的凸包内部,领航船之间可以自由运动,内部跟随船则保持相对位置关系。它比单独的领航-跟随结构更稳健,因为约束来自一组领航者而不是单个参考点,某个领航者出问题时整体队形不容易崩溃。
预设性能约束解决的是“队形误差控制得怎么样”的问题。单纯要求误差最终趋向零是不够的,工程上还关心瞬态性能:误差不能超调太大、收敛不能太慢、稳态精度要在给定范围内。预设性能就是给误差画出一条“约束走廊”,让误差始终在里面走,不走出去。
动态预设性能则是在原始预设性能基础上的扩展。静态预设性能通常用一条指数衰减的边界函数描述,收敛速度是常数。动态版本让边界函数的收敛速率也可以随着系统状态实时调节,好处是:系统离目标远时误差边界收敛慢一点,避免控制量过大;接近目标时再加快边界收缩,保证快速收敛。简单说,就是对“误差约束走廊”本身也做了自适应控制。
这三个模块组合起来,最终的实现层次就是:
- 建立无人船的数学模型(运动学/动力学层)。
- 定义基于图论的包容误差。
- 给包容误差设计动态预设性能边界。
- 设计控制器,让误差在性能边界内收敛到零。
- 用Matlab仿真验证稳定性和性能。
2. 数学模型搭建:从无人船运动学模型开始
2.1 三自由度无人船模型
大多数这类论文采用三自由度水面船模型,位置量是北东坐标系下的 x、y 和艏向角 ψ,速度量为纵荡速度 u、横荡速度 v 和艏摇角速度 r。运动学部分写出来就是:
dx/dt = u·cos(ψ) − v·sin(ψ) dy/dt = u·sin(ψ) + v·cos(ψ) dψ/dt = r动力学部分在复现时比较麻烦,因为不同论文力模型差别很大。有的考虑科氏力、阻尼矩阵和水动力系数,有的则简化为全驱动或者欠驱动模型。我做复现的第一步建议:如果目标是验证包容控制器本身,先把运动学模型跑通,把期望速度指令当成系统输入,动力学层用一个带惯性延迟的一阶环节近似替代,或者干脆在这版仿真里直接用运动学模型。这样能把控制算法的核心逻辑先验证了,后面再往里面加动力学模型就不容易排查错误。
2.2 图论建模:谁和谁通信
包容控制里,通信拓扑用有向图或无向图描述,节点是船,边是通信链路。我们通常把船分成两组:领航船(leader)和跟随船(follower)。领航船没有入边,即它们不接收其他船的指令;跟随船至少存在一条可达领航锚点的路径。
邻接矩阵 A、度矩阵 D、拉普拉斯矩阵 L = D − A 是Matlab里最先要写出来的量。尤其要注意节点编号顺序。我自己的代码统一按照“先领航后跟随”的顺序排列节点,这样拉普拉斯矩阵可以分块写作:
L = [ Lll Llf Lfl Lff ]包容控制成立的条件是 Lff 非奇异(对应拓扑中每个跟随者都能被领航者影响),并且 −Lff 是 Hurwitz 矩阵,这决定了误差动态的收敛性。在Matlab里检查这两条,用eig(Lff)看一眼特征值实部是否全为负/或者最小奇异值是否大于零,可以提前排除一大批拓扑设计错误。
2.3 包容误差定义
对于跟随船 i,基于邻居位置定义包容误差:
e_i = Σ_{j ∈ Ni} a_ij·(p_j − p_i)其中 p_i = [x_i; y_i],p_j 是邻居船的位置,a_ij 是邻接矩阵里的权重。把所有跟随者的误差堆叠起来,可以写成紧凑形式:
e_f = Lfl·p_l + Lff·p_f这个式子看着抽象,但实际上它的含义很直观:每个跟随者朝它的邻居靠拢,而领航者起到“边界锚点”的作用,最终使得所有跟随者被包含在领航者构成的凸包内。代码实现时,直接在仿真循环里遍历每个跟随者计算加权误差,再把误差传进预设性能函数即可。
3. 动态预设性能约束:核心细节与参数设计
3.1 性能函数的形式
常规预设性能约束,需要给误差 e(t) 设置上下两条衰减边界。我习惯用指数性能函数:
ρ(t) = (ρ0 − ρ∞)·e^(−λt) + ρ∞其中 ρ0 是初始边界宽度,ρ∞ 是稳态误差允许上限,λ 决定收敛速度。约束条件是 −ρ(t) < e(t) < ρ(t)。只要初始误差落在 ±ρ0 范围内,控制器就能保证误差一直不越界,最终进入 ±ρ∞ 的稳态精度范围。
动态预设性能的关键改动,是让 λ 不再固定,而是让它随着误差或滑模面状态自适应变化。一种常见的设计是:
λ_dyn(t) = λ0 + λadj(t)λadj 根据误差大小实时更新。误差偏大时降低有效收敛速度,防止初始阶段控制量过大导致执行器饱和;误差进入稳定逼近阶段后,恢复甚至提升收敛速度。这样比固定 λ 的目标跟踪效果更平滑,尤其在无人船启停转向这种工况切换频繁的场景里,实际效果差别明显。
3.2 误差变换
约束条件 −ρ < e < ρ 是时变不等式,直接用约束后的误差做控制设计不太顺手。通用做法是引入误差变换函数,比如对数变换:
z = 1/2 · ln((ρ + e)/(ρ − e))当 e 趋于上边界 ρ 时,z 趋于正无穷;当 e 趋于下边界 −ρ 时,z 趋于负无穷。所以,只要保证 z 有界,原误差就始终在约束走廊内。控制目标从“让 e → 0”变成“让 z → 0”,这种做法把时变不等式约束转化成了无约束镇定问题,不仅简化设计,还方便后面做李雅普诺夫稳定性分析。
这里要特别提醒:初始误差必须在界限内。如果 ρ0 选小,比如初始位置误差超过 ρ0,变换函数直接出现负数开方或者除零问题,仿真就会立刻发散。所以调参数时必须先跑一遍开环系统,量一下不同初始条件下的最大误差,再回头把 ρ0 放宽到它的1.5倍以上。
3.3 为什么动态比静态好用
从复现的观测结果看,静态性能边界在误差初值较大时,控制器容易在初始阶段给出非常大的控制指令,导致船速、加速度超过逼真模型的合理范围。动态版本因为能调节收敛速度,相当于给控制器加了一个“软启动”,控制量峰值明显降低。
4. 包容控制律设计与稳定性分析
4.1 控制目标
设 p_i 是船的位置,设计控制指令 u_i,使得对所有跟随者:
lim(t→∞) |e_i(t)| ≤ ρ∞并且整个过程中 |e_i(t)| 始终小于 ρ(t)。这里 u_i 可以直接是运动学层面的速度指令,也可以扩展成动力学层面的力和力矩。
4.2 控制器推导
在运动学层面,我设计的控制器如下:
对跟随者 i,定义误差变换后的变量 z_i,然后取候选李雅普诺夫函数:
V = 1/2 · z_i²对其求导:
dV/dt = z_i · ż_i根据变换 z_i 对时间的导数可以推出包含控制输入的关系式。如果设虚拟控制量为期望速度 v_d,i,则可以把控制律确定为:
v_d,i = κ_i · z_i + ρ_dot_i 的补偿项 + 邻居速度的加权项具体展开写,就是:
v_d,i = −k1_i · z_i + (1/ρ_i) · Σ a_ij·(v_j − v_i) 形式的相关项注意这里不同论文符号差异很大,千万不要照搬公式,要自己从变换求导一步步推。我在复现时就因为把 ρ 的导数符号写反,导致仿真里误差持续震荡不收敛,排查了两个小时。
设计完成后,用李雅普诺夫函数验证稳定性。把控制律代回 dV/dt,如果得到 dV/dt ≤ −c·V + δ,其中 c > 0,δ 是扰动上界,就说明系统是输入到状态稳定的,误差最终衰减到可接受的稳态精度内。
5. Matlab仿真代码实现
5.1 整体代码结构
我建议把代码拆成几个函数:主仿真脚本、系统动力学、图论初始化、性能函数更新、控制器计算。这样每个模块单独调试,不会一团乱。
%% 主仿真脚本 main_sim.m clear; clc; close all; % 1. 参数配置 N = 5; % 船总数 leaderN = 2; % 领航船数 followerN = N - leaderN; % 跟随船数 Tfinal = 50; % 仿真时长 dt = 0.01; t = 0:dt:Tfinal; % 2. 初始化邻接矩阵与拉普拉斯矩阵 [A, L] = initGraph(N, leaderN); % 3. 性能函数参数 rho0 = 3; rhoInf = 0.1; lambda = 0.5; lambda0 = 0.3; kappa = 0.5; % 自适应项增益 % 4. 初始位置与速度 p0 = [ ... ]; % 根据自己的仿真场景设定 v0 = zeros(2, N);核心是两层循环:外层遍历时间步,内层遍历船的序号。按这个结构,代码不复杂,但运行时需要耐心调参数。
5.2 图论初始化函数
我习惯先写在脚本外单独文件initGraph.m,生成无向拓扑。注意顺序:前 leaderN 个是领航船,后 followerN 个是跟随船。
function [A, L] = initGraph(N, leaderN) A = zeros(N, N); % 示例:领航船1连接跟随船3、4,领航船2连接跟随船4、5 edges = [1 3; 1 4; 2 4; 2 5; 3 4; 4 5]; for k = 1:size(edges,1) i = edges(k,1); j = edges(k,2); A(i,j) = 1; A(j,i) = 1; end D = diag(sum(A,2)); L = D - A; fprintf('Lff 最小特征值实部: %f\n', min(real(eig(L(leaderN+1:end, leaderN+1:end))))); end如果拓扑设计不当,Lff 不具有正定性,后面就算控制律再完美也无法收敛,这一步是整篇代码最重要的前置检查。
5.3 动态预设性能函数
这个函数每次更新时间步都会用到,输入当前误差和上一步性能边界状态,输出当前边界、收敛速度和误差变换值。
function [rho, lambdaEff, z] = performanceUpdate(e, rhoPrev, params) % 简单实现:根据误差大小动态调节收敛速率 rhoInf = params.rhoInf; rho0 = params.rho0; lambda0 = params.lambda0; kappa = params.kappa; lambda_dyn = lambda0 / (1 + kappa * abs(e)); rho = rhoInf + (rho0 - rhoInf) * exp(-lambda_dyn * params.t); lambdaEff = lambda_dyn; % 对数误差变换 if abs(e) >= rho error('误差越界,检查性能函数初值或控制参数'); end z = 0.5 * log((rho + e) / (rho - e)); end这里把 ρ0 设计成常数,ρ∞ 是稳态精度。想做得更工程化,可以让 ρ∞ 也动态更新,但复现阶段用平滑衰减版本就够了。
5.4 控制器实现
function vDes = controller(i, p, vNeighbors, rho, rhoDot, z, params) k1 = params.k1(1); % 控制增益 % 根据邻居位置计算包容误差 e = zeros(2,1); for j = neighbors{i} e = e + A(i,j) * (p(:,j) - p(:,i)); end % 变换 + 补偿 + 镇定 zVec = 0.5 * log((rho + e) ./ max(rho - e, 1e-6)); vDes = -k1 * zVec * rho - rhoDot * (e ./ rho); end这段代码是简化实现,重点在于展示变换和控制律的耦合关系。实际工程上还应该在控制律里加一个邻居速度的前馈项,否则跟踪太依赖增益大小,增益一旦调大就容易震荡。
6. 复现结果、参数调优与报错排查
6.1 预期仿真结果
调好参数的仿真,通常能看到这样几个现象:
- 所有跟随船被限制在领航船围成的凸包内,即使领航船做曲线运动,队形也能保持不破裂。
- 跟随船位置误差 e(t) 始终被两条边界 ±ρ(t) 包住,并且随着 ρ(t) 变窄,误差通道越来越紧。
- 控制输入曲线在初始阶段没有剧烈尖峰,这是动态性能约束与静态约束最直观的区别。
6.2 调参经验
参数优先级:先调 ρ0 和 ρ∞,再调 λ,最后调控制增益 k1。很多人一上来就动 k1,结果越调越振。我个人的经验,ρ∞ 决定最终的编队稳态精度,如果航迹任务对队形精度要求不高,ρ∞ 可以放宽到 0.5 米级别,不必追求越小越好,因为 ρ∞ 越小,周围误差边界越紧,系统对扰动越敏感。
控制增益 k1 建议从 0.1 附近开始,每次乘2往上试,记录超调量。另一个重要变量是时间步长 dt,我推荐至少取 0.01 秒,误差变换在对数函数分母接近零时对离散化误差极其敏感,步长过大直接影响稳定性。
6.3 高频问题速查
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| 仿真初始即报错,提示取对数无效 | 初始误差超过 ρ0 | 调大 ρ0,或者调整初始船间距 |
| 误差振荡发散但无明显越界 | k1 过大 | 减小增益,增大 dt 采样密度 |
| 误差长期不收敛到 ρ∞ | 图拓扑不连通或 Lff 特征值不对 | 检查邻接矩阵,打印 Lff 特征值 |
| 控制量初始尖峰巨大 | λ 固定值偏大,或缺少动态调节 | 使用 λ_dyn 自适应项,降低初始 λ0 |
| 运行缓慢 | 仿真时间步太细且内层循环规模大 | vector化邻居循环,或使用 matlabFunction 加速 |
6.4 实操中最容易出问题的细节
最容易踩坑的是把 e 和 ρ 的符号搞混。预设性能的约束条件是 −ρ < e < ρ,很多人在写误差变换代码时把符号写反,或者把 ρ 的导数符号搞反,结果仿真里误差贴边反弹,表现为“误差一直贴着边界走却掉不回来”的诡异现象。排查这类问题,建议在控制器代码里临时加一行fprintf打印 e、rho、z 的值,看哪一步开始跳动,定位会快很多。
另一个隐蔽问题是:对数变换时,如果离散采样导致 e 短暂越界,函数直接崩溃。实际工程处理是给变换函数加一个边界保护量,比如abs(e) > 0.999*rho时就饱和处理。这样虽然理论上不那么严谨,但仿真不会因为数值抖动中断。
7. 后续扩展方向与个人体会
这个项目的复现版本到这里就算完整了,但你要拿这套代码去跑更贴近实际的任务,还有几个可以继续完善的地方。一个是把运动学控制升级为动力学控制,加上水动力系数和舵角约束,这样你就能讨论真实推进器饱和下性能边界是否依然成立。另一个是加入扰动观测器,外界的风浪流扰动是无人船实际航行里绕不开的问题,论文里经常用神经网络逼近或者扩张状态观测器来做补偿,在Matlab里加一个扩张状态观测器模块,改动量并不大,但说服力会强很多。
我在做这个项目时还有一个体会:复现论文和看懂论文是两码事。很多控制论文里符号体系非常自治,照抄到代码里往往对不上,最快的解决方式是回到最基本的求导定义,重新推一遍误差变换的时变关系。这套代码跑通之后,你再回过头去看论文,很多当时觉得跳跃的推导,其实是作者省略了中间代数步骤。做复现研究,最有价值的不是最后的仿真图,而是被迫把所有细节补齐的过程。希望这篇整理能帮你少走点弯路。