返回课程总览

实验 07 · MATLAB + 状态空间

MATLAB 状态空间迭代

把一维数据向量扩展为二维状态,用 Python 参考结果检查 MATLAB 矩阵递推。

完整讲义

Lab 07:MATLAB 状态空间迭代

Lab 6 已经在熟悉的数据集上练习了 MATLAB 脚本、数组索引、逐元素运算、结果导出和人工 验证。本实验保留这些工具,只改变数学对象:从一组互不影响的数据,过渡到随时间演化的 二维状态。你将第一次使用矩阵乘法表达耦合递推,并用独立 Python 结果检查 MATLAB 实现。

开始前复用 Lab 4 的 Git 检查点流程:用 git status 确认工作区干净;完成跨语言验证后, 按相同方法提交 Lab 7 检查点。

学习目标

  • 把 \(\boldsymbol{x}(k+1)=A\boldsymbol{x}(k)\) 翻译成可运行程序。
  • 区分数据向量与状态向量,理解状态为何需要用列向量表示。
  • 理解 MATLAB 中矩阵乘法、列索引和预分配的作用。
  • 用独立的 Python 结果检查 MATLAB 实现。
  • 生成可供后续实验复用的数据表、图像和实验记录。

从数据向量到状态向量

Lab 6 的 values 是一个 1 × 6 行向量,每个位置保存一条独立观测。本实验的 x0 是 一个 2 × 1 列向量,两个分量共同描述同一时刻的系统状态。两者都用数组保存,但数组方向 和运算含义不同:

Lab 6 中的写法 MATLAB 写法与差别
values = [2, 4, 5, 7, 11, 13] x0 = [2; -1];逗号分隔列,分号开始新的一行
values(1) 取第一条数据 x(:, 1) 取第一个时刻的全部状态分量
centeredValues .^ 2 逐元素平方 A * x0 用矩阵乘法组合两个状态分量
数据位置不表示时间递推 x(:, k + 1)x(:, k) 计算,列位置对应时间

表达式 A * x0 不是把两个数组对应元素相乘,而是按照矩阵的行与列形成两个新的状态分量。 这一步同时复用了 Lab 6 对数组形状的检查,并引入了状态空间模型所需的新运算。

先在 MATLAB 的 Files 面板中打开 labs/lab07/,然后在 Command Window 输入:

A = [0.9, 0.1; -0.2, 0.8];
x0 = [2; -1];
x1 = A * x0

输出应为列向量 [1.7; -1.2]。这一步只用于熟悉矩阵输入和命令窗口;正式结果仍必须由 state_iteration.m 从头生成,不能依赖工作区中手动留下的变量。

状态空间中的“状态”

“状态”不是某个固定的物理量,而是在模型给定后,决定未来演化所需的信息。如果两个 历史过程在当前时刻具有相同状态,并且以后受到相同输入,那么状态空间模型会给出相同的 后续演化。本实验没有外部输入,因此当前的 \(\boldsymbol{x}(k)\) 和矩阵 \(A\) 已足以确定 \(\boldsymbol{x}(k+1)\)。

二维状态 \(\boldsymbol{x}(k)=[x_1(k),x_2(k)]^{\mathsf T}\) 可以表示位置与速度、两个温度,或经过 坐标变换得到的两个数学变量。变量的实际含义由模型定义;“二维”只说明需要两个数共同描述 当前状态。

对一般矩阵 \(A=[a_{ij}]\),第 \(i\) 行给出第 i 个下一状态的计算规则,第 \(j\) 列表示 第 j 个当前状态如何进入各个更新式:

\[ x_i(k+1)=\sum_j a_{ij}x_j(k). \]

因此本实验的矩阵不是四个互不相关的数字,而是两条耦合更新式:

\[ \begin{aligned} x_1(k+1)&=0.9x_1(k)+0.1x_2(k),\\ x_2(k+1)&=-0.2x_1(k)+0.8x_2(k). \end{aligned} \]

目录结构

在现有课程项目中创建如下结构。Starter 压缩包提供两个源文件的起点;实验输出由你运行程序后生成。

labs/lab07/
  python_state_reference.py
  state_iteration.m
  results/
    python-state-reference.csv
    state-history.csv
  figures/
    state-history.png
  notes.md

可以从课程总览下载 Starter 文件,只把 starters/lab07/ 中的文件复制到当前实验目录。

小知识:状态不是系统的全部历史

状态不是把过去发生的一切全部保存下来,而是在模型成立时足以决定未来演化的信息。选择什么 量作为状态取决于模型;状态选得不足,未来就无法只由当前状态确定,选得过多则会带来冗余。

理论问题

考虑离散状态方程:

\[ \boldsymbol{x}(k+1)=A\boldsymbol{x}(k),\qquad \boldsymbol{x}(0)=\begin{bmatrix}2\\-1\end{bmatrix},\qquad A=\begin{bmatrix}0.9&0.1\\-0.2&0.8\end{bmatrix}. \]

\(\boldsymbol{x}(k)\) 是二维列向量。每一步都用同一个线性映射 \(A\) 生成下一状态。这个模型很简单,但已经包含后续控制实验的基本骨架:状态、系统矩阵、初值和时间索引。

先手算第一步:

\[ \boldsymbol{x}(1)=A\boldsymbol{x}(0) =\begin{bmatrix}1.7\\-1.2\end{bmatrix}. \]

这一行手算结果是程序的第一个检查点。若首步就不一致,应先检查矩阵、向量方向和索引,而不是继续画图。

公式到代码

Python 常用一维列表表示向量,而 MATLAB 的控制论代码通常明确使用列向量:

A = [0.9, 0.1; -0.2, 0.8];
x0 = [2; -1];

把全部状态预先放在一个 2 × (stepCount + 1) 矩阵中:

x = zeros(2, stepCount + 1);
x(:, 1) = x0;

for k = 1:stepCount
    x(:, k + 1) = A * x(:, k);
end

这里 x(:, k) 表示第 k 列。数学中的 \(\boldsymbol{x}(0)\) 存在 MATLAB 第 1 列,数学中的 \(\boldsymbol{x}(k)\) 对应 MATLAB 第 k+1 列。这种偏移必须在笔记中写清楚。

预分配不是为了改变数学结果,而是提前固定数据形状,避免循环不断扩展数组,也使“每列是一时刻的状态”成为明确约定。

从状态矩阵到可复现结果

Lab 6 已经区分 Command Window、脚本、工作区和磁盘文件。这里沿用同一约定:Command Window 只用于首步试算和形状检查,正式轨迹必须由脚本生成。关闭 MATLAB 或执行 clear 后,工作区 变量会消失,但 .m 脚本、CSV 和图像仍保存在磁盘上。

本实验的完整数据流是:

A、x0 和 stepCount
        ↓ 状态递推
状态矩阵 x
        ↓ table / writetable
results/state-history.csv
        ↓ plot / exportgraphics
figures/state-history.png

mfilename("fullpath") 取得当前脚本的位置,fullfile 按 Windows 路径规则组合目录和文件名; writetable 把带列名的数据表写成 CSV,exportgraphics 把图窗保存成可用于报告的图像。这些 函数把计算与输出位置写进脚本,使结果不依赖 MATLAB 从哪个目录启动。

MATLAB 实验

  1. 先运行 python_state_reference.py,记录首步和第 12 步状态,并确认它生成 results/python-state-reference.csv
  2. 在 MATLAB 中打开并运行一次 Starter,观察 TODO 尚未完成时的输出。
  3. 完成 state_iteration.m 中的状态循环。
  4. 在命令窗口检查 size(x) 是否为 2 × 13
  5. writetablekx1x2 写入 results/state-history.csv
  6. 绘制两个状态分量随 k 变化的曲线,补充坐标名、图例和标题。
  7. 保存 figures/state-history.png,执行 clear 后重新运行一次脚本。

不要把命令窗口里人工修改过的变量当成程序输入。脚本应从矩阵和初值开始独立生成全部结果。

数学验证

至少完成四项检查:

检查 方法 能发现的问题
首步检查 比较 x(:,2)[1.7; -1.2] 矩阵抄写、向量方向或索引错误
形状检查 assert(isequal(size(x), [2, 13])) 状态历史的行列约定错误
跨语言检查 比较 Python 与 MATLAB 第 12 步 循环次数或更新次序错误
递推残差 计算 norm(x(:,k+1)-A*x(:,k)) 输出与公式不一致

递推残差 \(\lVert\boldsymbol{x}(k+1)-A\boldsymbol{x}(k)\rVert_2\) 接近机器精度,只能说明保存的轨迹满足这条递推关系;它不能单独证明所有初值下的长期性质。

结果与记录

state-history.csv 看成原始数值结果,把 state-history.png 看成快速预览。notes.md 写四段短说明:模型、参数、验证方法和限制。图注可以写成:

> 二维离散系统在给定初值下的 12 步状态轨迹。MATLAB 结果与独立 Python 实现逐点一致。

避免写“系统已经被证明稳定”。当前实验只观察一个矩阵、一个初值和有限步数。

OpenCode 审计

复用 Lab 4 的 OpenCode 工作流。输入 /models,确认当前模型是 DeepSeek V4 Pro;按 Tab 进入 Plan 模式后再进行只读检查。先独立完成首步计算,再使用中文提示词:

请检查 labs/lab07/state_iteration.m。

背景:数学模型是 x(k+1)=A*x(k),每一列保存一个时刻的二维状态。
请完成以下工作:
1. 解释每段 MATLAB 代码对应哪一个数学对象;
2. 检查数学索引 k 与 MATLAB 列索引之间是否偏移正确;
3. 检查状态矩阵如何经过 table、writetable 和 exportgraphics 生成 CSV 与图像;
4. 只提出必要修改,不要改变给定矩阵和初值;
5. 说明这些数值检查能验证什么、不能证明什么。

如果 OpenCode 只说“代码看起来正确”,继续追问它给出具体的首步数值、数组尺寸和残差 表达式。接受计划后按 Tab 进入 Build 模式。修改完成后,在 VS Code 的第二个 PowerShell 终端运行 git statusgit diff -- labs\lab07,再决定是否保留改动。

完成标准

  • Python 与 MATLAB 的第 12 步结果在显示精度内一致。
  • 两个 CSV 都有 13 行数据,图像可由脚本重新生成。
  • 能根据矩阵的一行解释一个下一状态如何由当前状态计算。
  • 能区分脚本、工作区、CSV 和图像各自保存的内容。
  • notes.md 能准确区分公式实现、数值验证和稳定性证明。
  • 提交 Lab 7 检查点后,git status 显示工作区干净。