
这几年实验室里做无人船集群方向复现研究的同学不少真正让人头疼的往往不是推导而是论文里一句话带过的仿真参数。今天整理的这个“无人船编队在动态预设性能约束下的包容控制”复现过程算是我踩过不少坑之后沉淀下来的完整记录。文章会从数学模型、约束设计、控制律推导一路讲到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(leaderN1:end, leaderN1: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里加一个扩张状态观测器模块改动量并不大但说服力会强很多。我在做这个项目时还有一个体会复现论文和看懂论文是两码事。很多控制论文里符号体系非常自治照抄到代码里往往对不上最快的解决方式是回到最基本的求导定义重新推一遍误差变换的时变关系。这套代码跑通之后你再回过头去看论文很多当时觉得跳跃的推导其实是作者省略了中间代数步骤。做复现研究最有价值的不是最后的仿真图而是被迫把所有细节补齐的过程。希望这篇整理能帮你少走点弯路。