缘起

我做了关于PID的漫画后,有读者回复说可以继续再讲讲“自抗扰控制”。这是我之前完全没听说过的知识,于是就想测试一下学习。我的目的也不高,就是大致了解这是怎么回事,建立直观的印象,理解其大致的原理,反正日后需要具体实现用的时候可以让AI帮我写程序。

整个流程大致是:

  1. 画个漫画 → 2.让AI写示例代码 → 3. 让AI详细讲解代码 → 4. 画出数据流程图 → 5. 追问细节 → 6. 让AI出个习题

画个漫画

An image to describe post

An image to describe post

An image to describe post

An image to describe post

An image to describe post

An image to describe post

An image to describe post

An image to describe post

An image to describe post


代码

我仍然不太明白这个漫画中的讲解。请为我写一个wolfram示例,比如我们有一个点光源,有两枚控制光路的反射镜,将点光源发出的光汇聚到目标点上,有一个观测结构,可以测量出在目标点上两个反射镜反射的光点位置。用LADRC应该如何调整呢?有必要的话,我们还可以加入一个干扰源,比如光线传播路径上还有一个其他的按时间变化的东西,例如厚度按sin(t)变化的结构或者其他合适的干扰源。

这是一个非常棒的要求。为了让你真正理解LADRC(线性自抗扰控制)是如何工作的,我们将编写一个完全自包含的Wolfram Language程序

这个程序将模拟你描述的场景:

  1. 物理系统:双轴控制系统(模拟两个反射镜或一个Tip-Tilt变形镜),控制光斑在X-Y平面上的位置。
  2. 干扰源:一个随时间变化的“正弦波干扰”(模拟大气湍流或光路上的周期性振动)+ 随机噪声。
  3. 控制器:LADRC控制器,它将尝试抵消干扰,将光斑锁定在原点 (0,0)(0,0)

核心逻辑说明 (对应漫画概念)

  • 总扰动 (Total Disturbance, ff): 包含了我们人为加入的正弦波干扰,以及系统本身的惯性。
  • ESO (扩张状态观测器): 代码中的核心函数。它不看具体的干扰源是什么,只看“预测位置”和“实际位置”的差值,反推由于干扰导致了多少偏差 (z3z_3)。
  • 控制律: u=u0z3b0u = \frac{u_0 - z_3}{b_0}。把估算出的 z3z_3 减掉,从而在数学上把系统变成一个简单的积分器。

Wolfram Language 代码

ClearAll["Global`*"];

(* ========================================================== *)
(* 1. 核心算法定义 (优化版) *)
(* ========================================================== *)

(* 干扰源:为了在短时间内看清效果,我们让频率稍微快一点 *)
disturbance[t_] := 20.0 * Sin[2.0 * Pi * 2.0 * t] + (* 2Hz 主震动 *)
                   RandomReal[{-5, 5}];             (* 噪声 *)

(* LADRC初始化 *)
createLADRC[wc_, wo_, b0_, dt_] := <|
   "z1" -> 0., "z2" -> 0., "z3" -> 0., 
   "b0" -> b0, "dt" -> dt,
   "beta1" -> 3 * wo, "beta2" -> 3 * wo^2, "beta3" -> wo^3,
   "kp" -> wc^2, "kd" -> 2 * wc
|>;

(* LADRC 单步更新函数 - 保持原逻辑不变 *)
stepLADRC[ctrl_, yMeasured_, setpoint_] := 
  Module[{z1, z2, z3, e, u0, u, dt, b0, beta1, beta2, beta3, kp, kd, 
    newZ1, newZ2, newZ3},
   {z1, z2, z3, dt, b0} = Lookup[ctrl, {"z1", "z2", "z3", "dt", "b0"}];
   {beta1, beta2, beta3, kp, kd} = Lookup[ctrl, {"beta1", "beta2", "beta3", "kp", "kd"}];
   
   e = z1 - yMeasured;
   u0 = kp * (setpoint - z1) - kd * z2;
   u = (u0 - z3)/b0; (* 核心:减去估算的扰动 z3 *)
   
   (* ESO 更新 *)
   newZ1 = z1 + dt * (z2 - beta1 * e);
   newZ2 = z2 + dt * (z3 - beta2 * e + b0 * u);
   newZ3 = z3 + dt * (-beta3 * e);
   
   {<|ctrl, "z1" -> newZ1, "z2" -> newZ2, "z3" -> newZ3|>, u}
];

(* ========================================================== *)
(* 2. 高速仿真循环 (使用 Reap/Sow) *)
(* ========================================================== *)

(* 参数设置 *)
T = 3.0;        (* 缩短为3秒,足以看清稳定过程 *)
dt = 0.01;      (* 步长 10ms,云端运行更轻松 *)
steps = Round[T/dt];
b_true = 10.0;  (* 物理系统增益 *)
wc = 15.0;      (* 控制器带宽 *)
wo = 80.0;      (* 观测器带宽 *)

(* 运行仿真并收集数据 *)
(* Reap 负责收集 Sow 种下的数据,这是云端运行的关键优化 *)
simulationResult = Reap[
   (* 初始化变量 *)
   ctrlX = createLADRC[wc, wo, b_true, dt];
   state = <|"x" -> 0., "vx" -> 0., "x_ol" -> 0., "vx_ol" -> 0.|>;
   
   Do[
    t = i * dt;
    dist = disturbance[t];
    
    (* --- 1. LADRC 控制 --- *)
    {ctrlX, u} = stepLADRC[ctrlX, state["x"], 0.0];
    
    (* --- 2. 物理演化 --- *)
    (* 闭环 (有控制) *)
    acc = b_true * u + dist;
    state["x"] += state["vx"] * dt;
    state["vx"] += acc * dt;
    
    (* 开环 (无控制,只有干扰) *)
    acc_ol = dist;
    state["x_ol"] += state["vx_ol"] * dt;
    state["vx_ol"] += acc_ol * dt;
    
    (* --- 3. 收集数据 (高速) --- *)
    (* 格式: {t, 闭环X, 开环X, 估算扰动z3, 真实扰动f} *)
    Sow[{t, state["x"], state["x_ol"], ctrlX["z3"], dist}];
    
    , {i, 1, steps} (* 循环结束 *)
   ];
][[2, 1]]; (* 提取 Reap 的结果列表 *)

(* 转换数据列以便绘图 *)
dataT  = simulationResult[[All, 1]];
dataX  = simulationResult[[All, 2]]; (* LADRC *)
dataOL = simulationResult[[All, 3]]; (* Open Loop *)
dataZ3 = simulationResult[[All, 4]]; (* Estimated Disturbance *)
dataF  = simulationResult[[All, 5]]; (* Real Disturbance *)

(* ========================================================== *)
(* 3. 结果可视化 *)
(* ========================================================== *)

(* 图1: 控制效果对比 *)
p1 = ListLinePlot[{
    Transpose[{dataT, dataOL}], 
    Transpose[{dataT, dataX}]
   },
   PlotStyle -> {
     {Red, Opacity[0.5], Thickness[0.005]}, (* 开环:红色乱晃 *)
     {Cyan, Thickness[0.003]}               (* LADRC:青色稳定 *)
   },
   PlotLegends -> {"Open Loop (Uncontrolled)", "LADRC Stabilized"},
   Frame -> True, FrameLabel -> {"Time (s)", "Position Error"},
   PlotLabel -> Style["Effect: Canceling the 'Storm'", Bold, 14],
   GridLines -> Automatic, Background -> Black,
   BaseStyle -> {FontFamily -> "Arial", FontColor -> White},
   ImageSize -> Medium
];

(* 图2: 观测器能力 (ESO) *)
p2 = ListLinePlot[{
    Transpose[{dataT, dataF}], 
    Transpose[{dataT, dataZ3}]
   },
   PlotStyle -> {
     {Red, Opacity[0.4], Thickness[0.008]}, (* 真实扰动 *)
     {Green, Dashed, Thickness[0.002]}      (* ESO估算 *)
   },
   PlotLegends -> {"Real Disturbance f(t)", "ESO Estimate z3"},
   Frame -> True, FrameLabel -> {"Time (s)", "Force"},
   PlotLabel -> Style["Mechanism: ESO Tracking Unknown Disturbance", Bold, 14],
   GridLines -> Automatic, Background -> Black,
   BaseStyle -> {FontFamily -> "Arial", FontColor -> White},
   ImageSize -> Medium
];

(* 输出 *)
Column[{p1, p2}, Spacings -> 1]

代码讲解 (硬核补习班风格)

1. 模拟的场景

我们建立了一个 disturbance[t] 函数。想象这个函数就是漫画里“雷锤”抱怨的引擎共振和外面的暴风雨。它由不同频率的正弦波组成,让光路发生偏移。

2. ESO 的魔法 (stepLADRC 中的逻辑)

请注意代码中的 z3

  • 我们并没有告诉ESO干扰是正弦波。
  • ESO只是不断比较 z1 (估算位置) 和 yMeasured (传感器读数)。
  • 如果发现 yMeasured 跑得比预期快,ESO 就会认为:“肯定有个外力在推它!”
  • 它把这个“推测出的外力”存入 z3
  • 看图表2 (右上角):你会惊讶地发现,绿色的虚线(ESO估算值)几乎完美地覆盖了红色的实线(真实干扰)。这就是漫画里 \mu 说的“看见了总扰动”。

3. 控制器的反击

u = (u0 - z3)/b0 这一行。

  • 一旦 ESO 算出了 z3(比如干扰是 +20),控制器就在输出电压里减去对应的量。
  • 结果 (图表1 左上角):红色的线是“关掉LADRC”时的光斑,它像疯了一样乱跑(被干扰推着走)。蓝色的线是“开启LADRC”后的光斑,它被紧紧锁死在原点附近。

4. 为什么这适合自适应光学?

  • 在自适应光学中,大气湍流是随机且高频的。传统的 PID 需要积分器 I 来消除误差,但积分器有滞后(必须先积累误差)。
  • LADRC 通过 ESO 直接观测干扰的变化趋势,在误差积累变大之前就进行了抵消。这就是为什么蓝线能保持如此平稳的原因。

如何运行

将此代码复制并在 Wolfram Cloud 或本地 Mathematica 中运行。你会看到全黑背景的工业风图表,直观展示 LADRC 如何将狂暴的干扰驯服为静止的点。

An image to describe post

代码讲解

现在请一点一点为我讲解这个程序代码,我对wolfram语言也不熟悉。请耐心些

没问题。Wolfram Language (Mathematica) 的语法非常独特,它更像是在写数学公式,而不是像 C++ 或 Python 那样写指令。

为了让你完全理解,我们将这段代码像“拆解一台精密仪器”一样,分成五个模块来讲解。我会尽量用自然语言配合编程概念来解释。


模块一:起手式与“制造敌人”

ClearAll["Global`*"];
  • 解释:这是Wolfram程序的标准开头。
  • 作用:清除内存中以前定义的所有变量。就像把黑板擦得干干净净,防止以前写的 x = 5 干扰现在的计算。
(* 干扰源 *)
disturbance[t_] := 20.0 * Sin[2.0 * Pi * 2.0 * t] + RandomReal[{-5, 5}];
  • 语法f[x_] := ... 是定义函数的标准方式。t_ 后面的下划线表示 t 是一个占位符(参数)。:= (SetDelayed) 意味着“每次调用时才计算”,而不是现在就定死。
  • 物理含义:这是我们在漫画里提到的“总扰动”中的一部分(外部干扰)。
    • 20.0 * Sin[...]:一个振幅为20、频率为2Hz的有规律震动(模拟引擎共振)。
    • RandomReal[{-5, 5}]:一个随机的噪声(模拟风或大气湍流)。
  • 形象理解:这就是那个不断推搡反射镜的“坏蛋”。

An image to describe post


模块二:初始化控制器 (打造容器)

createLADRC[wc_, wo_, b0_, dt_] := <|
   "z1" -> 0., "z2" -> 0., "z3" -> 0., 
   "b0" -> b0, "dt" -> dt,
   "beta1" -> 3 * wo, "beta2" -> 3 * wo^2, "beta3" -> wo^3,
   "kp" -> wc^2, "kd" -> 2 * wc
|>;
  • 语法<| Key -> Value, ... |>。这在 Wolfram 语言中叫 Association (关联)。它非常像 Python 的 Dictionary (字典) 或 JSON 对象。
  • 作用:我们将控制器需要的所有东西打包成一个“包裹”。
    • z1, z2, z3:这是控制器的记忆(状态)。z1是估算位置,z2是估算速度,z3最重要的估算扰动。初始都设为0。
    • beta1, beta2...:这是根据高志强教授的“带宽参数化法”自动计算出的参数。你只需要给它 wo (观测带宽),它自己算出内部需要的系数。
  • 形象理解:这就像是叶子(Leaf)在编写代码时,定义了一个名为“LADRC控制器”的结构体(Struct)。

模块三:控制器的思考过程 (核心算法)

这是最复杂的部分,我们逐行看:

stepLADRC[ctrl_, yMeasured_, setpoint_] := 
  Module[{z1, z2, z3, e, u0, u, ...}, (* 1. 局部变量声明 *)
   
   (* 2. 提取记忆 *)
   {z1, z2, z3, dt, b0} = Lookup[ctrl, {"z1", "z2", "z3", "dt", "b0"}];
   {beta1, beta2, beta3, kp, kd} = Lookup[ctrl, {...}];
   
   (* 3. 观测误差 *)
   e = z1 - yMeasured;
   
   (* 4. 计算理想控制量 u0 (PD控制) *)
   u0 = kp * (setpoint - z1) - kd * z2;
   
   (* 5. 核心一击:抗扰 *)
   u = (u0 - z3)/b0; 
   
   (* 6. 更新记忆 (ESO) *)
   newZ1 = z1 + dt * (z2 - beta1 * e);
   newZ2 = z2 + dt * (z3 - beta2 * e + b0 * u);
   newZ3 = z3 + dt * (-beta3 * e);
   
   (* 7. 返回结果 *)
   {<|ctrl, "z1" -> newZ1, "z2" -> newZ2, "z3" -> newZ3|>, u}
];
  1. Module[{...}, ...]:这就好比在函数内部划定了一个“私有工作区”,这里的变量(如 u)用完就扔,不会污染外面的环境。
  2. Lookup:从刚才那个“包裹”里把数据取出来。
  3. e = z1 - yMeasured:ESO(观测器)看了一眼自己的预测 (z1) 和现实 (yMeasured) 差了多少。
  4. u = (u0 - z3)/b0全篇代码的灵魂
    • u0 是我们想要怎么走。
    • z3 是 ESO 发现的“捣乱的力”。
    • 我们直接在指令里减去 z3。如果风往右吹 (+20),我们就往左使劲 (-20)。这在数学上就把干扰抵消了。
  5. newZ...:这是根据欧拉法(Euler Integration)预测下一时刻的状态,为下一次循环做准备。
  6. 返回值:函数最后输出两样东西:{更新后的控制器包裹, 这一刻该输出的电压 u}

模块四:仿真循环 (时间流逝)

这里有一个为了在云端运行而优化的特殊技巧:ReapSow

simulationResult = Reap[
   (* ... 初始化代码 ... *)
   
   Do[
    t = i * dt;
    dist = disturbance[t];
    
    (* ... 物理计算 ... *)
    
    (* 收集数据 *)
    Sow[{t, state["x"], state["x_ol"], ctrlX["z3"], dist}];
    
    , {i, 1, steps} (* Do 循环结束 *)
   ];
][[2, 1]];
  • Do[..., {i, 1, steps}]:这是标准的循环,i 从 1 走到 steps
  • Sow[...] (播种):在 Mathematica 中,如果你用 AppendTo 向一个列表里添加数据,随着列表变长,速度会越来越慢(因为要复制内存)。Sow 就像是农夫播种,它只管把数据“扔”出去,速度极快。
  • Reap[...] (收割):它包裹着整个循环。等循环跑完了,它负责把刚才所有 Sow 扔出去的数据一次性收集起来,打包成一个列表。
    • 这对于资源受限的云端环境至关重要,能让代码快几十倍。

在循环内部的物理逻辑很简单:

  • acc = b_true * u + dist:牛顿第二定律 (F=maF=ma)。加速度 = 我们的控制力 + 外部干扰力。
  • state["x"] += state["vx"] * dt:位置 = 旧位置 + 速度 ×\times 时间。

模块五:可视化 (画图)

p1 = ListLinePlot[{
    Transpose[{dataT, dataOL}], 
    Transpose[{dataT, dataX}]
   }, ...];
  • Transpose:我们的数据本来是按行存的 {t, x, ...}Transpose 把它转置,变成 {t1, t2...}, {x1, x2...},方便画图。
  • ListLinePlot:这是 Wolfram 最强大的绘图函数之一。
    • 它能直接画出一系列点组成的线。
    • 后面的 PlotStyle, Frame 等都是装修参数,为了让图表看起来像漫画里那种“高科技工业风”(黑色背景、亮色线条)。

总结:整个程序的流程

  1. 准备:清理内存,定义干扰函数。
  2. 设置:创建一个空的控制器(包裹),设定好带宽参数。
  3. 循环(比如模拟 3 秒钟,每步 0.01 秒):
    • 环境:计算当前时刻的风有多大。
    • 感知与思考:LADRC 控制器看一眼当前位置,估算干扰 (z3),计算反击指令 (u)。
    • 行动:将 u 施加给物理系统,算出下一刻的位置。
    • 记录:用 Sow 把这一刻的数据扔进篮子。
  4. 收割Reap 收集所有数据。
  5. 展示:把数据画成图表。

数据流动图

这是一个非常敏锐的问题。理解数据流向是读懂程序的关键。

你需要明白三个角色的关系:

  1. 主循环 (Do):这是“时间”的管理者,也是数据的搬运工。
  2. stepLADRC 函数:这是一个“加工厂”。它不保存数据,它只负责接收旧数据,算出新数据,然后扔回去。
  3. simulationResult (Reap/Sow):这是旁边的“记账员”,负责把过程记录下来。

下面我为你画两张图。第一张是宏观的数据循环图,第二张是函数内部的加工流程图


第一张图:宏观循环 (The Loop)

这张图展示了在 Do 循环的**每一次迭代(i)**中,数据是如何在 ctrlX、物理系统和记账员之间流动的。

graph TD
    subgraph Loop [Do Loop 每一次时间步 t]
    
    %% 准备数据
    StateNode[当前状态 state] -->|提供 x| InputMeasure(传感器测量值 state_x)
    CtrlNode[旧控制器对象 ctrlX] -->|提供记忆 z1 z2 z3| InputCtrl(控制器记忆包)
    TargetNode[目标值 0.0] -->|提供 0.0| InputTarget(设定目标)

    %% 调用函数
    InputCtrl & InputMeasure & InputTarget --> Process{{"调用函数: stepLADRC"}}

    %% 函数输出
    Process -->|输出 1| NewCtrl(更新后的 ctrlX)
    Process -->|输出 2| OutputU(控制电压 u)

    %% 物理演化
    OutputU --> Physics[物理计算 F=ma]
    Disturbance[干扰 disturbance] --> Physics
    Physics -->|更新| NewState[下一刻状态 state]

    %% 这里的循环 (虚线表示变量被更新覆盖)
    NewCtrl -.->|覆盖旧变量| CtrlNode
    NewState -.->|覆盖旧变量| StateNode

    %% 记账 (Sow)
    OutputU -.-> Record
    NewCtrl -.-> Record
    StateNode -.-> Record
    Disturbance -.-> Record
    Record(Sow: 播种数据) -->|扔进篮子| Basket[("Reap buffer (临时篮子)")]
    end

    %% 循环结束
    Basket -->|最终提取| FinalResult[simulationResult]

关键点解析:

    • 传入 (Input)stepLADRC 需要三个原材料:
    1. 你是谁 (ctrlX):包含上一时刻算出的扰动 z3 等记忆。
    2. 你在哪 (state["x"]):当前的物理位置。
    3. 去哪里 (0.0):目标位置。
  • 传出 (Output):函数吐出两个产品:
    1. 新的自己 (newCtrl):更新了记忆(z1, z2, z3 变了)。代码里用 {ctrlX, u} = ... 把旧的 ctrlX 覆盖掉了。
    2. 动作指令 (u):这用来驱动物理引擎。

第二张图:微观加工 (Inside stepLADRC)

这张图展示了数据进入函数内部后,具体发生了什么化学反应。这是 LADRC 的核心逻辑。

flowchart LR
    %% 输入端
    subgraph INPUTS [输入参数]
        direction TB
        I1[ctrl_: 旧记忆 z1, z2, z3]
        I2[yMeasured_: 真实位置]
        I3[setpoint_: 目标 0]
    end

    %% 内部逻辑
    subgraph PROCESS [stepLADRC 内部处理]
        direction TB
        
        %% 第一步:看误差
        I1 & I2 --> NODE1(观测误差 e = z1 - yMeasured)
        
        %% 第二步:算期望控制
        I3 & I1 --> NODE2(PD计算 u0 = kp*误差 - kd*速度)
        
        %% 第三步:核心抗扰
        NODE2 --> NODE3(计算控制量 u)
        I1 --"取旧的 z3"--> NODE3
        NODE3 --"u = (u0 - z3) / b0"--> RESULT_U[控制量 u]

        %% 第四步:更新记忆 (ESO)
        NODE1 & RESULT_U & I1 --> NODE4(ESO 更新公式)
        NODE4 --"预测下一刻"--> RESULT_CTRL[新记忆 z1, z2, z3]
    end

    %% 输出端
    INPUTS --> PROCESS
    RESULT_U --> OUT1((输出 u))
    RESULT_CTRL --> OUT2((输出 ctrl))

对应代码的一步步解析:

我们在主循环里写了这行代码:

{ctrlX, u} = stepLADRC[ctrlX, state["x"], 0.0];

具体的“参数传递”映射关系如下:

代码中的参数位置 传入的实参 (Main Loop) 函数内的形参 (Function) 含义
第 1 个 ctrlX ctrl_ 记忆包。函数第一件事就是用 Lookup 把里面的 z1, z2, z3 拆出来用。
第 2 个 state["x"] yMeasured_ 传感器读数。函数用它和内部预测的 z1 做减法,算出 e
第 3 个 0.0 setpoint_ 目标。函数用它来计算 PD 控制部分 (setpoint - z1)。

函数处理完后的“返回”映射关系:

函数返回的列表 赋值给 (Main Loop) 发生了什么?
第 1 个元素 ctrlX 自我更新。函数把计算好的 newZ1... 打包成一个新的 Association,主循环接收它并覆盖旧的 ctrlX。这样下一次循环时,用的就是最新的记忆了。
第 2 个元素 u 执行动作。主循环接收这个数值,把它代入 b_true * u + dist 计算物理加速度。

关于 simulationResult

simulationResult 其实是局外人

  1. 它不参与计算。
  2. stepLADRC 算出 uctrlX 之后,主循环里有一行 Sow[...]
  3. Sow 就像拍快照。它拍下了这一瞬间的时间 t、位置 x、以及刚刚被 stepLADRC 算出来的 ctrlX["z3"]
  4. 等几百次循环全部跑完,Reap 把这些快照装订成册,交给了 simulationResult

所以,simulationResult事后的实验报告,而 stepLADRC实时的操作员

希望这个逐行拆解能让你看懂这段“魔法咒语”背后的逻辑!Wolfram 语言虽然符号多,但它的核心思

想非常接近数学原本的描述。

追问1:LADRC参数含义

更细节的问题:
createLADRC[wc_, wo_, b0_, dt_] := <|
"z1" -> 0., "z2" -> 0., "z3" -> 0.,
"b0" -> b0, "dt" -> dt,
"beta1" -> 3 * wo, "beta2" -> 3 * wo^2, "beta3" -> wo^3,
"kp" -> wc^2, "kd" -> 2 * wc
|>;

之中,

  1. z1是位置,z2是速度,如果是其他问题,是否就是z1是系统的输出,z2是输出的一阶导数?
  2. b0是什么?如何确定?
  3. 我看几个参数beta1,beta2,beta3是由wo确定的,系数3,3,1,指数1,2,3这是规定的吗?还是根据问题不同会有不同?
  4. kp, kd是由wc给出的,系数和指数也是规定好的吗?wo是观测带宽,wc是什么?
  5. 什么是观测带宽?

这五个问题非常硬核,直击 LADRC(线性自抗扰控制)的灵魂。这正是韩京清教授提出ADRC,以及后来高志强教授提出“带宽参数化”(Bandwidth Parameterization)试图解决的核心痛点:让控制理论从“玄学调参”变成“工程科学”

我们逐一解答,继续用我们之前的机甲/光学系统作为比喻。


1. z1,z2z_1, z_2 到底是什么?

结论:是的。对于二阶系统(最常见的物理系统),z1z_1 是输出,z2z_2 是输出的一阶导数。

  • z1z_1 (位置):它对应系统的直接输出 yy。在我们的例子里,就是光斑的位置。
  • z2z_2 (速度):它对应输出的变化率 y˙\dot{y}。在我们的例子里,就是光斑移动的速度。
  • z3z_3 (总扰动)这是多出来的那个“扩张状态”。在物理上,原本系统只有位置和速度两个状态,但我们人为加了一个 z3z_3 来代表“所有让系统偏离预期的未知力量”(摩擦力、风、模型误差等)。

推广一下:

LADRC 的阶数取决于对象的物理本质。

  • 温度控制(一阶系统):加热器功率直接改变温度变化率。此时,z1z_1 是温度,z2z_2 就是总扰动。
  • 电机/运动控制(二阶系统):力改变加速度(速度的导数)。此时,z1z_1是位置,z2z_2是速度,z3z_3是总扰动。这是最常用的配置。

2. b0b_0 是什么?如何确定?

结论:b0b_0 是控制增益(Control Gain),代表“输入对输出的控制能力”。它不需要非常精确。

物理含义:

公式是 y¨=f+b0u\ddot{y} = f + b_0 \cdot u

  • uu 是你施加的电压(比如 1V)。
  • b0b_0 是这一电压能产生多大的加速度。
  • 如果是牛顿第二定律 F=maa=1mFF=ma \Rightarrow a = \frac{1}{m} F,那么 b0b_0 大致就是 1m\frac{1}{m}(质量的倒数)。

如何确定?

  1. 理论计算:如果你知道反射镜有多重、致动器推力常数是多少,可以直接算出来。
  2. 实验测试:给系统一个 1V 的阶跃电压,看它加速有多快。
  3. LADRC的优势不需要准!
    • 假设真实的 bb 是 10。
    • 你设 b0=10b_0 = 10,完美。
    • 你设 b0=8b_0 = 8b0=12b_0 = 12,系统依然非常稳定。因为 ESO 会把 b0b_0 的误差也当成“内部扰动”(z3z_3) 给估算出来并抵消掉。
    • 工程经验:通常宁可把 b0b_0 设得比真实值一点点,也不要太小,以防止控制量 uu 过大导致振荡。

3. β1,β2,β3\beta_1, \beta_2, \beta_3 的系数 3, 3, 1 是规定的吗?

结论:是的,在“带宽参数化”方法中,这是根据二项式定理死规定的。

数学原理(不用怕):

ESO(观测器)本质上是一个数学方程。为了让这个观测器稳定且好用,我们需要配置它的“极点”(Eigenvalues)。

高志强教授提出:与其去凑三个复杂的参数,不如让观测器的所有极点都落在同一个位置 ωo-\omega_o

数学上,一个三阶方程如果有三个重根 ωo-\omega_o,它的特征多项式就是:

(s+ωo)3=s3+3ωos2+3ωo2s+ωo3 (s + \omega_o)^3 = s^3 + 3\omega_o s^2 + 3\omega_o^2 s + \omega_o^3

对比我们的 ESO 方程特征式:

s3+β1s2+β2s+β3 s^3 + \beta_1 s^2 + \beta_2 s + \beta_3

一一对应,你就得到了:

  • β1=3ωo\beta_1 = 3\omega_o
  • β2=3ωo2\beta_2 = 3\omega_o^2
  • β3=ωo3\beta_3 = \omega_o^3

意义:这把原本复杂的极点配置问题,简化成了一个旋钮——ωo\omega_o。你只需要调这一个数,剩下的系数自动就是最优比例。


4. kp,kdk_p, k_d 的系数和指数也是规定的吗?ωc\omega_c 是什么?

结论:是的,同样源于二项式定理。ωc\omega_c 是控制器带宽。

控制律使用的是 PD(比例-微分)控制:u=kp(ry)kdy˙u = k_p(r-y) - k_d \dot{y}

我们希望闭环控制系统的行为像一个标准的二阶系统,且没有超调(Critically Damped)。

数学上,这意味着闭环传递函数的两个极点都放在 ωc-\omega_c

(s+ωc)2=s2+2ωcs+ωc2 (s + \omega_c)^2 = s^2 + 2\omega_c s + \omega_c^2

对比标准二阶系统方程 s2+kds+kps^2 + k_d s + k_p,得到:

  • kd=2ωck_d = 2\omega_c
  • kp=ωc2k_p = \omega_c^2

ωc\omega_c (Controller Bandwidth) 是什么?

它决定了系统听话的速度

  • ωc\omega_c 越大,系统对指令的反应越快(从 0 跑到 1 需要的时间越短)。
  • 但如果 ωc\omega_c 太大,超过了硬件的极限(比如电机转不了那么快,或者采样率跟不上),系统就会震荡甚至炸机。

5. 什么是观测带宽 (ωo\omega_o)?

结论:它是 ESO “这只眼睛”的刷新率和敏锐度。

  • 形象比喻

    • ωc\omega_c (控制器) 是你的手速。你希望手越快越好,能迅速把车摆正。
    • ωo\omega_o (观测器) 是你的眼速(或者大脑处理视觉信号的速度)。
  • 关系

    • 眼睛必须比手快! 如果你手很快,但眼睛这秒钟才看到上一秒的路况,你一定会翻车。
    • 通常规定:ωo310×ωc\omega_o \approx 3 \sim 10 \times \omega_c
    • 也就是说,ESO 的估算速度,必须比控制器的调节速度快一个数量级,这样控制器拿到 z3z_3 时,它才是“新鲜”的、准确的。
  • 副作用

    • 既然 ωo\omega_o 越快越好,为什么不设成无限大?
    • 因为 ωo\omega_o 越大,对噪声(Noise)越敏感。
    • 如果 ωo\omega_o 太大,传感器里一点点随机的电磁噪声(Random Noise)都会被当成“巨大的扰动”,导致电机疯狂抖动。
    • 所以,调参就是在“反应速度”和“抗噪能力”之间找平衡。

总结表格

参数 全称 作用 设定依据
b0b_0 系统增益估值 告诉控制器力气有多大 物理估算,宁大勿小
ωc\omega_c 控制器带宽 决定系统反应多快 (手速) 根据任务需求,不能超过硬件极限
ωo\omega_o 观测器带宽 决定扰动估算多快 (眼速) 通常设为 3103\sim10 倍的 ωc\omega_c
β1,2,3\beta_{1,2,3} ESO增益 保证观测器收敛 自动计算 (3ωo,3ωo2,ωo33\omega_o, 3\omega_o^2, \omega_o^3)
kp,dk_{p,d} 控制器增益 保证控制平稳无超调 自动计算 (ωc2,2ωc\omega_c^2, 2\omega_c)

这就是 LADRC 的美妙之处:虽然内部数学(状态空间、极点配置)很复杂,但最终暴露给用户的,只有 b0b_0(物理属性)、ωc\omega_c(想跑多快)和 ωo\omega_o(观测多快)这三个直观的参数。

追问2:

(* ESO 更新 *)
newZ1 = z1 + dt * (z2 - beta1 * e);
newZ2 = z2 + dt * (z3 - beta2 * e + b0 * u);
newZ3 = z3 + dt * (-beta3 * e);

这里是如何得到newZ1, newZ2, newZ3的?为何是这样的公式?newZ1, newZ2, newZ3是什么?后续如何使用?


这三行代码是 LADRC 的心脏——扩张状态观测器 (ESO) 的离散化实现。

为了让你彻底明白,我们得把这层窗户纸捅破。这里面包含了三个层面的逻辑:

  1. 物理模型(世界原本是怎么运作的?)
  2. 观测器原理(我们怎么去猜这个世界?)
  3. 数值计算(怎么把数学公式写成计算机代码?)

1. 物理模型:世界原本的样子

在 ESO 的眼里,它假设这个系统(光路控制)遵循以下规律:

  • 位置变化率 = 速度
    x˙1=x2 \dot{x}_1 = x_2
  • 速度变化率 = 加速度。而加速度由两部分组成:总扰动 (x3x_3) + 我们的控制力 (b0ub_0 u)
    x˙2=x3+b0u \dot{x}_2 = x_3 + b_0 u
  • 总扰动变化率 = 未知(假设它是某个导数 hh,但在离散的一瞬间可以假设它基本不变)。
    x˙3=h(t) \dot{x}_3 = h(t)

这就是我们试图追踪的“真理”。


2. 观测器原理:预测 + 修正

ESO 的工作原理就像是一个带 GPS 导航的司机

  • zz 是司机脑子里的预测路线
  • yy 是 GPS 告诉司机的真实位置
  • e=z1ye = z_1 - y预测误差(司机发现自己偏离了 GPS 显示的位置)。

ESO 的核心逻辑公式(连续形式)如下:

第一行:位置的观测 (z1z_1)

z˙1=z2β1e \dot{z}_1 = z_2 - \beta_1 \cdot e

  • z2z_2:根据当前速度,我预测下一秒位置会变。
  • β1e-\beta_1 e修正项
    • 如果 e>0e > 0(说明我预测的位置 z1z_1 比实际 yy 跑得远了),那么 β1e-\beta_1 e 就是负数,把我的预测往回拉。
    • 这就是负反馈

第二行:速度的观测 (z2z_2)

z˙2=z3+b0uβ2e \dot{z}_2 = z_3 + b_0 u - \beta_2 \cdot e

  • z3z_3:我认为有一个干扰力在推我。
  • b0ub_0 u:我知道我踩了油门 (uu),这会让我加速。
  • β2e-\beta_2 e修正项。如果位置算错了,说明我速度也算错了,赶紧修。

第三行:扰动的观测 (z3z_3) —— 最天才的一步

z˙3=β3e \dot{z}_3 = - \beta_3 \cdot e

  • 逻辑:模型里本来没有 z3z_3 的来源(我们不知道风从哪吹)。
  • 推理:如果你发现即使修正了位置和速度,预测值 z1z_1 依然和真实值 yy 有偏差(e0e \neq 0),那肯定是有一个未知的“鬼”(扰动)在作祟
  • ESO 直接把这个“位置偏差”通过积分(累加)转化为了“扰动估算值”。
  • 这就是为什么 ESO 能算出未知的干扰:它把所有的“算不准”都归结为 z3z_3

3. 数值计算:从数学到代码 (Euler Method)

计算机不能处理连续的微积分 z˙\dot{z},它只能一步一步算(时间步长 dt)。

我们使用最简单的欧拉法 (Euler Method)

新值=旧值+变化率×时间间隔 \text{新值} = \text{旧值} + \text{变化率} \times \text{时间间隔}

z(k+1)=z(k)+z˙(k)dt z(k+1) = z(k) + \dot{z}(k) \cdot dt

现在对照你的代码:

代码行 1

(* 物理意义:下刻位置 = 当前位置 + dt * (速度 - 修正) *)
newZ1 = z1 + dt * (z2 - beta1 * e);

这直接对应 z˙1=z2β1e\dot{z}_1 = z_2 - \beta_1 e

代码行 2

(* 物理意义:下刻速度 = 当前速度 + dt * (扰动 + 控制力 - 修正) *)
newZ2 = z2 + dt * (z3 - beta2 * e + b0 * u);

注意:这里用了 z3z_3 (当前估计的扰动) 和 uu (控制力) 来推算速度的变化。对应 z˙2=z3+b0uβ2e\dot{z}_2 = z_3 + b_0 u - \beta_2 e

代码行 3

(* 物理意义:下刻扰动 = 当前扰动 + dt * (- 修正) *)
newZ3 = z3 + dt * (-beta3 * e);

对应 z˙3=β3e\dot{z}_3 = -\beta_3 e。这一步使得 z3z_3 不断逼近真实的扰动 ff


newZ1, newZ2, newZ3 是什么?

它们是 “下一时刻(t+dtt + dt)” 系统的状态估计值。

  • newZ1: 预测 10ms 后的光斑位置
  • newZ2: 预测 10ms 后的光斑移动速度
  • newZ3: 预测 10ms 后的总扰动大小(风力+摩擦+震动)。

后续如何使用?

在代码的结尾,你会看到这行(或者类似的返回结构):

<|ctrl, "z1" -> newZ1, "z2" -> newZ2, "z3" -> newZ3|>

这步操作叫做 “更新记忆”

  1. 闭环迭代
    当前的 stepLADRC 函数执行完毕后,这三个新值会被存入 ctrlX 结构体中。
  2. 下一轮循环
    Do 循环进行到 i+1 次,再次调用 stepLADRC 时:
    • 今天的 newZ 就变成了明天的 oldZ
    • ESO 基于这些更新后的值,再次和新的传感器数据对比,继续修正。
  3. 产生控制
    最关键的用法是在下一轮计算控制量 uu 时:
    u=u0z3b0 u = \frac{u_0 - \mathbf{z_3}}{b_0}

    控制器会直接调用这个刚刚算出来的 newZ3,把它从控制信号里减掉。

总结:

这三行公式就是 ESO “通过不断反省自己的预测误差,从而猜出外界干扰” 的数学实现过程。

练习

请给我出一个小的例题,我来编程试试能否解决


这就对了!动手写代码是理解控制算法最好的方式。

既然你已经掌握了基本的 LADRC 结构,我们来做一个稍微有点物理背景的练习题。


📝 练习题:不知情的“电梯” (The Ignorant Elevator)

场景描述:

你正在控制一台垂直升降的货运电梯。

  • 物理对象:电梯轿厢,质量 m=100 kgm = 100 \text{ kg}
  • 环境:地球重力 g=9.8 m/s2g = 9.8 \text{ m/s}^2(方向向下)。
  • 控制量:电机产生的向上拉力 FF (牛顿)。
  • 目标:将电梯从地面 (0 m0 \text{ m}) 拉升到 (20 m20 \text{ m}) 处,并稳稳停住。

物理方程:

根据牛顿第二定律 Fmg=maF - mg = ma,即:

a=Fmg a = \frac{F}{m} - g

或者写成我们在代码里用的标准形式 y¨=bu+f\ddot{y} = b \cdot u + f

y¨=1mbtrueu+(g+随机扰动)ftotal \ddot{y} = \underbrace{\frac{1}{m}}_{b_{true}} \cdot u + \underbrace{(-g + \text{随机扰动})}_{f_{total}}

你的任务(挑战点):

最关键的是:请不要告诉控制器有重力存在!

在你的控制器代码里,不要写任何关于 9.8 的数字。你要完全依赖 ESO (z3z_3) 自己去发现“咦?好像有一股神秘的力量在一直把电梯往下拉”。


🛠️ 编程填空

我已经为你准备好了框架,请复制下面的代码,并完成标有 (* TODO: ... *) 的部分。

需要填写的参数提示:

  1. 干扰函数:除了随机噪声,别忘了加上重力项(9.8-9.8)。
  2. btrueb_{true}:根据 a=F/mga = F/m - g,系数 bb 应该是多少?
  3. b0b_0:控制器以为的增益。你可以设得和 btrueb_{true} 一样,或者稍微大一点。
  4. setpointsetpoint:目标高度是多少?
ClearAll["Global`*"];

(* === 1. 定义物理环境 === *)
(* 真实的物理参数 *)
m = 100.0; (* kg *)
g = 9.8;   (* m/s^2 *)

(* 干扰函数:包括恒定的重力和随机风力 *)
disturbance[t_] := (* TODO: 在这里填空。提示:重力向下(-g) + 随机数 *) 
   0.0; (* 先写个0占位,请修改我 *)

(* === 2. LADRC 核心 (直接复用之前的,不用改) === *)
createLADRC[wc_, wo_, b0_, dt_] := <|
   "z1" -> 0., "z2" -> 0., "z3" -> 0., 
   "b0" -> b0, "dt" -> dt,
   "beta1" -> 3 * wo, "beta2" -> 3 * wo^2, "beta3" -> wo^3,
   "kp" -> wc^2, "kd" -> 2 * wc
|>;

stepLADRC[ctrl_, yMeasured_, setpoint_] := 
  Module[{z1, z2, z3, e, u0, u, dt, b0, beta1, beta2, beta3, kp, kd, 
    newZ1, newZ2, newZ3},
   {z1, z2, z3, dt, b0} = Lookup[ctrl, {"z1", "z2", "z3", "dt", "b0"}];
   {beta1, beta2, beta3, kp, kd} = Lookup[ctrl, {"beta1", "beta2", "beta3", "kp", "kd"}];
   
   e = z1 - yMeasured;
   u0 = kp * (setpoint - z1) - kd * z2;
   u = (u0 - z3)/b0; 
   
   newZ1 = z1 + dt * (z2 - beta1 * e);
   newZ2 = z2 + dt * (z3 - beta2 * e + b0 * u);
   newZ3 = z3 + dt * (-beta3 * e);
   
   {<|ctrl, "z1" -> newZ1, "z2" -> newZ2, "z3" -> newZ3|>, u}
];

(* === 3. 运行仿真 === *)
T = 5.0;       
dt = 0.01;
steps = Round[T/dt];

(* TODO: 填写参数 *)
b_true = (* TODO: 真实的系统增益 1/m *);
b_est  = (* TODO: 控制器估算的增益,可以等于 b_true *);
target = (* TODO: 目标高度 20米 *);

(* 带宽设置 (可以先用这一组,也可以自己调) *)
wc = 5.0; 
wo = 30.0; 

simulationResult = Reap[
   (* 初始化 *)
   ctrl = createLADRC[wc, wo, b_est, dt];
   state = <|"y" -> 0., "vy" -> 0.|>; (* 从地面 y=0 开始 *)
   
   Do[
    t = i * dt;
    
    (* 1. 计算环境干扰 *)
    dist = disturbance[t];
    
    (* 2. LADRC 控制 *)
    (* TODO: 把 target 填入函数调用的第三个参数 *)
    {ctrl, u} = stepLADRC[ctrl, state["y"], target];
    
    (* 3. 物理演化: a = b*u + f *)
    acc = b_true * u + dist;
    
    state["y"]  += state["vy"] * dt;
    state["vy"] += acc * dt;
    
    (* 4. 记录: {时间, 真实高度, 估算干扰 z3} *)
    Sow[{t, state["y"], ctrl["z3"]}];
    
    , {i, 1, steps}
   ];
][[2, 1]];

(* === 4. 画图验证 === *)
dataT  = simulationResult[[All, 1]];
dataY  = simulationResult[[All, 2]];
dataZ3 = simulationResult[[All, 3]];

(* 图1: 高度变化 *)
p1 = ListLinePlot[Transpose[{dataT, dataY}], 
   PlotStyle -> {Blue, Thickness[0.005]},
   PlotLabel -> "Elevator Position (Target: 20m)",
   Frame -> True, GridLines -> Automatic, ImageSize -> Medium];

(* 图2: ESO估算的干扰 (见证奇迹的时刻) *)
p2 = ListLinePlot[Transpose[{dataT, dataZ3}], 
   PlotStyle -> {Red, Thickness[0.005]},
   PlotLabel -> "What does ESO think the disturbance is?",
   Frame -> True, GridLines -> {{}, {-9.8}}, (* 我加了一条参考线 *)
   FrameLabel -> {"Time", "Estimated z3"},
   ImageSize -> Medium];

Column[{p1, p2}]

🚀 思考题 (运行后再看)

当你运行成功后,观察图2(红色的线):

  1. z3z_3 最终稳定在什么数值附近?
  2. 为什么是这个数值?(提示:重力加速度是多少?)
  3. 如果把 mm 改成 200kg,但不用告诉控制器,你觉得 z3z_3 会变吗?系统还能稳在 20米吗?

请将补全后的代码发给我,或者直接告诉我你的实验结果!