简介:这份离散控制Matlab代码围绕Hamilton-Jacobi-Bellman方程,给出牛顿策略迭代的Tensor Train实现,利用低秩张量分解缓解高维状态空间的计算与存储压力,面向有一定最优控制、数值PDE或科学计算基础的研究生与算法开发者。压缩包共15个文件、约24KB,主体为13个.m函数与脚本,另有README说明和许可证;求解器、谱微分、Legendre递推、交叉近似及一维/二维Allen-Cahn、Fokker-Planck测试脚本构成较完整的实验链路。README与Matlab help列出逐文件说明和调用语法,参数解析脚本支持交互录入模型与近似参数,便于复现数值实验、观察控制约束影响并做二次开发。已有1202人学习,可作为HJB数值求解与TT格式实现的参考。
1. 从高维 Bellman 方程说起:这套 TT-HJB 代码解决的是什么问题
状态维度到 6 维、每维 41 个网格点时,一张值函数表就是 41^6 个 double,约 38GB,传统离散动态规划还没开始迭代,内存先翻车。这套离散控制 Matlab 代码把 Hamilton-Jacobi-Bellman 方程的值函数写成 Tensor Train 格式,用牛顿策略迭代在压缩后的张量空间里求解,目标很明确:让高维最优控制问题从“存不下”变成“能跑、能改、能验证”。它适合做最优控制、动态规划、科学计算的研究者和工程开发者,尤其是已经写过一维/二维 DP、想往 4 到 8 维推的人。系统开源代码里能看到 TT rounding、策略评估、策略改进和牛顿线性化几个关键环节,拿下来先跑最小算例,再换自己的动力学和代价函数,路径比较直接。
2. 技术栈与工程骨架:Matlab 里的 TT 格式、离散控制与牛顿策略迭代
2.1 Tensor Train 为什么能压住高维值函数
这个项目的技术栈不复杂:Matlab 做数值计算,Tensor Train 做值函数压缩,离散控制网格做策略搜索,牛顿策略迭代做外层求解。真正决定项目能不能落地的是 TT 格式:它把 d 维张量 V(x1,…,xd) 拆成一串三阶核心张量 G1(r0,r1,x1)…Gd(rd-1,rd,xd),相邻核心通过 TT rank r 连接。存储量从 O(n^d) 降到 O(d n r^2),当 r 远小于 n 时,高维值函数才有存下来的可能。
在 HJB 场景里,值函数通常不是完全随机的张量,光滑性给了 TT 截断的空间。牛顿策略迭代每步会做张量加法、逐点取最小、线性系统求解,这些操作都会让 TT rank 上涨,所以代码里必须有 rounding。常见做法是每次 Bellman 算子作用后调用一次 tt_round,用截断容差和最大 rank 两个参数卡住增长。下面这段示例骨架展示值函数从全网格到 TT 张量的创建过程,函数名按实际仓库调整,逻辑是通用的。
% 创建 d 维网格上的 TT 值函数示例 nDims = 6; % 状态维度 nGrid = 41; % 每维网格点数 ttTol = 1e-8; % TT 截断容差 maxRank = 32; % TT rank 上限 % 先生成初始值函数,实际项目里常从零或二次型初始化 xGrid = cell(1, nDims); for k = 1:nDims xGrid{k} = linspace(-1, 1, nGrid); end V0 = zeros(repmat(nGrid, 1, nDims)); % 小维度 smoke 测试才建议直接 zeros Vtt = tt_tensor(V0, ttTol, maxRank); % 转成 TT 格式 Vtt = tt_round(Vtt, ttTol, maxRank); % 再截断一次,防止初始 rank 过高这段代码里nDims和nGrid决定问题规模,ttTol控制截断精度,maxRank是内存上限的保险丝。tt_tensor和tt_round是 TT 工具层最常用的两个入口,前者把满张量转成 TT,后者在运算后压 rank。实际项目中不建议一上来就zeros满张量,高维下会直接吃满内存;更常见的做法是按 TT 核心逐维初始化,或者用低秩函数(如二次型)构造初始值函数。这个细节决定了代码在 6 维以上能不能启动。
TT 格式不是“免费的压缩”。rank 设得太小,值函数细节被抹掉,策略改进会震荡;rank 设得太大,内存和单步时间又上去。我一般会先跑一个 2 维或 3 维算例,把maxRank从 8 试到 32,观察残差和策略变化,再推到高维。项目里如果有check_tt_ranks.m这类工具,建议每轮迭代都打印最大 rank 和截断误差,别等跑完才发现 rank 已经涨到不可接受。
2.2 目录结构与模块职责
这类 Matlab 项目通常不会做得像大型框架,目录一般按“入口、配置、核心算法、工具、测试”分开。下面是一个常见的工程骨架,实际文件名可能略有差异,但职责划分基本对应。看目录时重点盯hjb/和core/两层:前者是问题定义,后者是 TT 线性代数。
| 路径 | 职责 | 二次开发时常改的地方 |
|---|---|---|
main.m | 运行入口,读参数、调牛顿策略迭代、保存结果 | 换算例、改输出目录 |
config/params_*.m | 维度、网格、控制集、γ、TT 容差、牛顿参数 | 新增算例配置 |
hjb/build_grid.m | 生成状态网格和控制网格 | 改状态范围、控制离散化 |
hjb/dynamics.m | 状态转移 f(x,u) | 换系统动力学 |
hjb/stage_cost.m | 单步代价 g(x,u) | 换代价函数 |
hjb/bellman_operator.m | 对控制取 min,构造 Bellman 右端 | 改边界处理、插值方式 |
hjb/policy_eval.m | 固定策略下的策略评估 | 改线性求解器 |
hjb/policy_improve.m | 策略改进,argmin Q | 改 tie-break、控制约束 |
hjb/newton_pi.m | 牛顿策略迭代主循环 | 改阻尼、收敛判据 |
core/tt_*.m | TT 加法、rounding、线性求解 | 换 TT 后端或并行 |
tests/ | smoke 测试、一维对比测试 | 加回归用例 |
调用链一般是main.m→newton_pi.m→policy_eval.m/policy_improve.m→bellman_operator.m→core/tt_*.m。这条链上最容易出问题的是“网格维度顺序”和“TT 核心 mode 顺序”不一致,后面避坑章会细讲。先跑tests/里的 smoke 用例,比直接上高维算例稳得多。测试目录里如果有一维解析解对比,优先跑那个,因为它能同时验证动力学、代价、Bellman 算子和 TT 截断。
工程上还有一个实用点:把配置写成函数返回结构体,而不是散落在脚本里的变量。这样同一个newton_pi可以跑一维 smoke、二维网格对比和六维正式算例,改参数不用动主循环。下面是一个配置函数的写法,字段名按项目习惯调整。
function params = params_smoke() % 最小 smoke 算例配置,先保证链路通,再谈精度 params.nDims = 2; % 状态维度 params.nGrid = 21; % 每维网格点数 params.uGrid = linspace(-1, 1, 9); % 控制离散集 params.gamma = 0.95; % 折扣因子 params.ttTol = 1e-8; % TT 截断容差 params.maxRank = 16; % TT rank 上限 params.maxNewton = 20; % 牛顿外层最大迭代 params.damping = 0.7; % 牛顿步阻尼 params.tolRes = 1e-6; % 残差收敛阈值 params.tolPolicy = 1e-4; % 策略变化收敛阈值 end这段配置里gamma接近 1 会让策略评估的线性系统条件数变差,damping就是给牛顿步留的后悔药。ttTol和maxRank要一起看:容差太松,残差降不下去;rank 上限太小,策略改进会缺信息。uGrid是离散控制集,控制越密策略越准,但每轮 Bellman 算子要遍历的控制数也越多,6 维以上要权衡。
2.3 一个典型运行入口和依赖检查
Matlab 项目最怕“换台机器就跑不起来”,常见原因是路径没加、工具箱缺失、TT 函数版本不一致。这个项目理论上只依赖 Matlab 基础数值能力和自带的 TT 工具层,不强制 Optimization Toolbox,但如果你把线性求解换成pcg或lsqr,就要确认相应函数可用。运行前先做三件事:把根目录和core/、hjb/加进路径;确认tt_tensor、tt_round能找到;跑一次tests/test_1d_smoke.m。
% main.m 的典型入口骨架 clear; clc; addpath(genpath(pwd)); % 把子目录加入路径 params = params_smoke(); % 读最小算例配置 check_env(); % 检查 TT 函数和网格顺序 [Vtt, policy, info] = newton_pi(params); fprintf('残差: %.3e, 策略变化: %.3e, 最大 rank: %d\n', ... info.resHist(end), info.policyHist(end), info.maxRank); save('result_smoke.mat', 'Vtt', 'policy', 'info');addpath(genpath(pwd))会把测试和工具目录都加进来,方便但有点粗暴,正式项目里建议只加必要目录。check_env不是摆设,它应该检查tt_tensor是否存在、nGrid和uGrid是否非空、gamma是否在 (0,1) 内。newton_pi返回的info里至少要有残差历史、策略变化历史和最大 rank,后面调参全靠这三个量。跑完先看info.resHist是不是单调下降,再看info.maxRank有没有顶到上限;如果 rank 一直顶格,说明maxRank设小了或者截断容差太松。
3. 核心链路拆解:从 Bellman 算子到牛顿线性系统
3.1 离散控制下的 Bellman 算子
离散控制 HJB 的 Bellman 形式是 V(x) = min_u { g(x,u) + γ V(f(x,u)) }。在网格上,f(x,u)不一定落在网格点上,常见做法是最近邻索引、线性插值或把动力学设计成网格映射。这个项目既然叫离散控制,控制集通常是有限个uGrid,状态网格是张量积网格。Bellman 算子要对每个控制算候选值函数,再逐点取最小。TT 的逐点最小不是线性运算,不能直接靠 TT 加法完成,常见做法是先把候选值函数转成 TT,做逐点 min 后再 rounding。
function [TV, uStar] = bellman_operator(Vtt, params) % 对离散控制集遍历,返回 Bellman 右端和最优控制索引 nU = numel(params.uGrid); candidates = cell(1, nU); for iu = 1:nU u = params.uGrid(iu); g = params.stage_cost(params.X, u); % 单步代价,网格张量 xNext = params.dynamics(params.X, u); % 状态转移 idxNext = state_to_index(xNext, params); % 映射到网格索引 Vnext = tt_eval_on_grid(Vtt, idxNext); % 在转移点取值 candidates{iu} = g + params.gamma * Vnext; % 候选 Q 值 end [TV, uStar] = tt_pointwise_min(candidates, params.ttTol, params.maxRank); TV = tt_round(TV, params.ttTol, params.maxRank); end这段代码里params.X是网格张量,state_to_index负责把连续状态映射到网格索引,tt_eval_on_grid在 TT 上取值。uStar是每个网格点上的最优控制索引,后面策略改进要用。tt_pointwise_min是逐点最小,不是 TT 线性运算,内部通常会把候选张量对齐后比较,再做截断。实际项目里state_to_index要处理越界:状态转移跑出网格时,可以钳制到边界、加惩罚项或拒绝该控制。边界处理没做好,策略会在边界附近抖动,残差看起来降了,实际策略不可用。
参数上gamma决定未来代价权重,uGrid决定控制分辨率,ttTol和maxRank决定压缩强度。tt_eval_on_grid的插值方式要和state_to_index保持一致:用最近邻就全用最近邻,用线性插值就全用线性插值,混用会让值函数出现莫名其妙的小台阶。我一般会先在一维算例上把bellman_operator和解析 Bellman 右端对比,误差在 1e-6 量级再往高维推。
3.2 牛顿策略迭代的线性化与 TT 求解
牛顿策略迭代的核心是把 Bellman 残差 F(V) = V - T(V) 压到零。对 F 做牛顿线性化,得到 J_F(V_k) δ = -F(V_k),其中 J_F = I - ∂T/∂V。策略固定时,∂T/∂V 对应折扣后的策略转移算子 γP_π,所以每一步牛顿步等价于解一个线性系统 (I - γP_π) δ = -F(V_k),再更新 V_{k+1} = V_k + αδ。α 是阻尼,防止步子太大导致残差反弹。策略改进和策略评估交替进行,外层再用牛顿步加速收敛,这是“牛顿策略迭代”在这个项目里的落地方式。
function [Vtt, policy, info] = newton_pi(params) % 牛顿策略迭代主循环 Vtt = params.init_value(params); % 初始值函数,通常是零或二次型 policy = ones(params.nGrid * ones(1, params.nDims)); % 初始策略索引 info.resHist = []; info.policyHist = []; info.maxRank = 0; for k = 1:params.maxNewton % 1) 固定策略,构造策略评估线性系统 A V = b [A, b] = build_policy_system(Vtt, policy, params); % 2) 牛顿步:解 (I - gamma P_pi) delta = -(V - T(V)) [TV, ~] = bellman_operator(Vtt, params); residual = Vtt - TV; rhs = -residual; % 线性化右端 delta = tt_als_solve(A, rhs, params.ttTol, params.maxRank); % 3) 阻尼更新 Vtt = tt_add(Vtt, params.damping * delta); Vtt = tt_round(Vtt, params.ttTol, params.maxRank); % 4) 策略改进 [~, policyNew] = bellman_operator(Vtt, params); policyChange = mean(policyNew(:) ~= policy(:)); policy = policyNew; % 5) 记录收敛信息 resNorm = tt_norm(residual) / max(1, tt_norm(Vtt)); info.resHist(end+1) = resNorm; info.policyHist(end+1) = policyChange; info.maxRank = max(info.maxRank, tt_max_rank(Vtt)); if resNorm < params.tolRes && policyChange < params.tolPolicy break; end end end这段主循环里build_policy_system组装策略评估线性系统,tt_als_solve用 TT 交替最小二乘解线性系统,tt_add做张量加法,tt_round控制 rank。damping取值通常在 0.3 到 1.0 之间:残差震荡就调小,收敛太慢就调大但别超过 1。tolRes和tolPolicy要一起用,只看残差容易在策略没稳定时提前退出。info.maxRank用来监控 TT rank 是否顶到maxRank,顶格说明压缩预算不够,后面要么放宽容差,要么提高上限。
这里有个容易混淆的点:策略评估本身是线性的,为什么还要牛顿?因为外层 Bellman 残差是非线性的,策略改进里的 min 带来了非线性。牛顿步是在当前策略附近对残差线性化,再用 TT 线性求解器解修正量。实际代码里也可以先做普通策略迭代,等残差降不动了再开牛顿加速,这样调试更直观。tt_als_solve的容差不要设得比tolRes还松,否则牛顿步解不准,外层残差会卡住。
3.3 策略改进与收敛判据
策略改进就是算 Q_u(x) = g(x,u) + γV(f(x,u)),然后对每个网格点取最小,记录最优控制索引。听起来简单,实际有两个细节:平局处理和策略抖动。离散控制集里两个控制产生几乎相同的 Q 值时,min会随意选一个,下一轮可能换另一个,策略变化率下不去。常见做法是加一个很小的二次惩罚项,或者在平局时保留上一轮策略。下面是一个策略改进的写法。
function [policyNew, Qmin] = policy_improve(Vtt, params, policyOld) % 策略改进:对每个控制算 Q,取最小并记录索引 nU = numel(params.uGrid); Q = cell(1, nU); for iu = 1:nU u = params.uGrid(iu); g = params.stage_cost(params.X, u); xNext = params.dynamics(params.X, u); idxNext = state_to_index(xNext, params); Q{iu} = g + params.gamma * tt_eval_on_grid(Vtt, idxNext); % 平局时轻微偏向旧策略,减少策略抖动 if nargin >= 3 Q{iu} = Q{iu} - 1e-12 * (iu == policyOld); end end [Qmin, policyNew] = tt_pointwise_min(Q, params.ttTol, params.maxRank); endpolicyOld是可选的旧策略索引,1e-12只是 tie-break,不影响正常 Q 值比较。tt_pointwise_min同时返回最小值和索引,索引张量不一定要 TT 压缩,低维下直接存满网格,高维下可以只存切片或按需重算。收敛判据建议三条一起看:残差范数、策略变化比例、TT rank 稳定性。残差降到tolRes以下但策略变化还在 1e-2 量级,说明值函数精度不够,策略还在换;策略变化很小但残差很大,可能是 TT 截断把关键信息抹了,得放宽容差或提高 rank。
4. 跑起来:环境准备、参数配置与第一个最小算例
4.1 Matlab 版本与工具箱检查
拿到源码后先别急着改高维算例,先确认环境。Matlab 版本用近几年的即可,老版本要注意arguments块、string 数组和隐式扩展这些语法差异。这个项目的核心是 TT 数值计算,理论上不需要额外工具箱;如果你把线性求解换成pcg,那还是基础 Matlab 自带。检查三件事:tt_tensor能不能找到、tt_round的签名是不是你预期的、tests/里有没有一维 smoke 测试。路径加完后跑一次check_env,比直接main报错再回头查快得多。
function ok = check_env() % 环境检查:TT 函数、路径、基本参数 ok = true; required = {'tt_tensor', 'tt_round', 'tt_add', 'tt_norm'}; for k = 1:numel(required) if exist(required{k}, 'file') ~= 2 fprintf('缺少函数: %s\n', required{k}); ok = false; end end if ~exist('params_smoke.m', 'file') fprintf('缺少 smoke 配置\n'); ok = false; end fprintf('环境检查完成,ok=%d\n', ok); endexist(..., 'file')返回 2 表示找到 m 文件。required列表按项目实际函数调整,别把测试函数写进去。检查通过后先跑tests/test_1d_smoke.m,一维算例小,出错信息直接,能快速定位是动力学、代价还是 TT 层的问题。如果一维都跑不通,高维不用试。
4.2 最小算例参数怎么配
最小算例的目标不是精度,是链路通。维度先 2,每维 21 个点,控制集 9 个点,gamma0.95,maxRank16,maxNewton20。这个规模下满张量也不大,方便和 TT 结果对比。配置表如下,字段名可以按项目实际调整。
| 参数 | 建议初值 | 作用 | 调大/调小的后果 |
|---|---|---|---|
nDims | 2 | 状态维度 | 调大内存和 rank 压力上升 |
nGrid | 21 | 每维网格点数 | 调大精度高但计算量指数增长 |
uGrid | 9 个点 | 控制离散集 | 调密策略准但 Bellman 遍历变慢 |
gamma | 0.95 | 折扣因子 | 接近 1 条件数变差,太小短视 |
ttTol | 1e-8 | TT 截断容差 | 太松残差降不下去,太紧 rank 涨 |
maxRank | 16 | TT rank 上限 | 太小策略缺信息,太大内存吃紧 |
maxNewton | 20 | 外层迭代上限 | 太小没收敛,太大浪费时间 |
damping | 0.7 | 牛顿步阻尼 | 震荡调小,收敛慢调大 |
tolRes | 1e-6 | 残差阈值 | 太严跑不完,太松结果不可用 |
tolPolicy | 1e-4 | 策略变化阈值 | 和残差一起看,别单独用 |
配置写好后用main.m跑,命令就是main或run('main.m')。跑完看三个输出:info.resHist是否下降、info.policyHist是否降到 1e-3 以下、info.maxRank是否远小于maxRank。如果残差第一轮就卡住,先检查gamma和damping;如果 rank 顶格,先放ttTol或提高maxRank。最小算例跑通后再把nDims加到 3、4,观察 rank 增长曲线,别一步跳到 8 维。
4.3 运行结果怎么读与第一次验证
结果文件里一般有Vtt、policy、info。第一次验证不要只看残差,要做三件事:画值函数切片、画策略切片、和一维/二维满网格 DP 对比。二维算例可以直接把 TT 值函数展开成满张量,和传统 DP 的值函数做差,误差在ttTol量级说明链路正确。三维以上满张量对比不现实,就退而求其次看 Bellman 残差和策略稳定性。
% 读取结果并做切片检查 load('result_smoke.mat', 'Vtt', 'policy', 'info'); Vfull = tt_to_full(Vtt); % 小维度才建议展开 slice = squeeze(Vfull(:, :, 11)); % 取中间控制或中间状态切片 figure; subplot(1,2,1); imagesc(slice); colorbar; title('值函数切片'); subplot(1,2,2); imagesc(squeeze(policy(:,:,11))); colorbar; title('策略切片'); figure; semilogy(info.resHist, 'o-'); grid on; title('残差历史');tt_to_full只在小维度用,高维展开会爆内存。squeeze去掉长度为 1 的维度,imagesc看值函数有没有异常尖峰。残差历史用半对数坐标,正常应该单调下降;如果出现平台或反弹,回到避坑章对应条目。第一次验证通过后,把params_smoke复制成params_2d,只改网格和控制集,确认同一套代码能复现,再开始换动力学。
5. 避坑与排查:TT-HJB 跑不通时先看这五个地方
5.1 现象:内存直接爆,或者跑两轮就卡死
现象很直接:Matlab 内存占用飙到几个 GB,或者第二轮迭代后单步时间从几秒变几分钟。原因通常是 TT rank 失控。Bellman 算子里的逐点最小、张量加法、策略评估线性系统都会让 rank 上涨,如果每次运算后不做tt_round,rank 会指数级增长。另一个常见原因是初始值函数用满张量zeros创建,6 维以上还没开始迭代就吃满内存。
解决分三步:第一,所有 TT 运算后强制tt_round,包括tt_add、tt_pointwise_min、线性求解的中间结果;第二,给maxRank设上限并每轮打印实际 rank,顶格就说明预算不够;第三,初始值函数用低秩构造,别用满张量。我一般会在newton_pi里加一行fprintf('iter=%d rank=%d res=%.2e\n', k, tt_max_rank(Vtt), resNorm),跑的时候一眼能看出是 rank 先炸还是残差先卡。
5.2 现象:一维算例对得上,二维结果整体错位
一维 smoke 通过,二维值函数图看起来“形状对但位置不对”,策略切片也有偏移。原因多半是网格维度顺序和 TT 核心 mode 顺序不一致。Matlab 的ndgrid第一维变化最快,而某些 TT 工具按 mode-1 对应第一维、mode-2 对应第二维,中间如果混用meshgrid、permute或reshape,顺序就乱了。更隐蔽的是state_to_index用线性索引,而 TT 取值按子索引,两边没对齐。
解决方式:统一用ndgrid生成网格,所有张量按同一维度顺序排列;在check_env里加一个二维测试函数,比如 V(x,y)=x+2y,展开后和 TT 取值对比;如果对不上,用permute调整而不是改索引公式。血泪经验是别在多个文件里各自reshape,把维度顺序约定写进配置注释,所有模块遵守同一个约定。一维测试只能验证算子逻辑,二维才能抓出顺序问题。
5.3 现象:残差下降几轮后开始震荡或反弹
残差曲线前几轮下降,后面上下跳,策略变化率也下不去。原因可能是牛顿步太大、gamma接近 1 导致线性系统病态,或者 TT 截断容差和牛顿容差不匹配。gamma=0.99时(I - γP_π)的条件数会明显变差,tt_als_solve解不准,牛顿方向就不可信。另一个原因是策略改进的平局抖动,两个控制 Q 值几乎相同,策略每轮换,残差跟着跳。
解决:先把damping从 0.7 降到 0.3 试,观察残差是否恢复单调;再检查tt_als_solve的容差是否小于tolRes,求解器太松会让牛顿步失真;策略抖动就加 tie-break,保留旧策略或加微小二次惩罚。如果gamma必须接近 1,考虑给线性系统加正则项,或者先用普通策略迭代把残差降到 1e-3 再开牛顿。别迷信“牛顿一定比普通策略迭代快”,条件数差的时候阻尼牛顿更稳。
5.4 现象:策略在边界附近来回跳,值函数出现尖峰
边界附近的策略切片像锯齿,值函数在边界出现异常尖峰。原因通常是状态转移跑出网格后处理不一致:有的控制被钳制到边界,有的控制被加了大惩罚,有的直接丢弃,导致 Q 值比较不公平。另一个原因是插值方式在边界外推,TT 取值出现非物理值。离散控制里控制集有限,边界控制更容易把状态推出网格。
解决:在dynamics.m或state_to_index里统一边界策略,要么全部钳制,要么全部加二次惩罚,别混用;在bellman_operator里对越界状态加惩罚项并记录越界比例,比例高说明网格范围或控制集设计不合理;值函数尖峰可以用tt_round压,但先解决物理原因。我一般会在info里加一个boundaryHitRate,每轮打印,超过 5% 就回去改网格范围。
5.5 现象:换了自己的动力学后,残差完全不降
最小算例能跑,换成自己的dynamics.m和stage_cost.m后残差第一轮就很大,后面不降。原因可能是动力学返回值维度不对、代价函数没有按网格张量返回、控制集单位不匹配,或者新系统的状态范围超出了原网格。Matlab 的隐式扩展有时会掩盖维度错误,比如g是列向量、Vnext是行向量,相加后变成矩阵,TT 层收到错误形状也不报错。
解决:给dynamics和stage_cost加输入输出尺寸断言,size(xNext)必须和size(params.X)一致,size(g)同理;先用一个控制点跑bellman_operator,把g、xNext、Vnext的尺寸打印出来;再检查新系统的状态范围,必要时改build_grid的上下界。换动力学后先跑一维退化版本,一维对得上再上二维。别跳过尺寸检查,这类错误排查起来最费时间。
6. 二次开发:把 TT-HJB 改造成你自己的最优控制实验台
二次开发先从替换问题定义开始,别动主循环。dynamics.m和stage_cost.m是两个最干净的切口:输入网格张量X和控制u,输出同尺寸的下一状态和单步代价。改完后先跑一维退化算例,把状态范围缩到一维、控制集保留,和解析解或满网格 DP 对比。一维对得上,再加维度。常见做法是给两个函数加尺寸断言,size(xNext)必须等于size(X),size(g)同理,这样隐式扩展不会偷偷把错误藏起来。
function xNext = dynamics(X, u) % 示例:线性系统 x_{k+1} = A x_k + B u,实际按你的系统改 A = 0.95; B = 1.0; xNext = A * X + B * u; assert(isequal(size(xNext), size(X)), 'dynamics 输出尺寸不匹配'); end function g = stage_cost(X, u) % 示例:二次代价,注意返回网格张量而不是标量 Q = 1.0; R = 0.1; g = Q * X.^2 + R * u.^2; assert(isequal(size(g), size(X)), 'stage_cost 输出尺寸不匹配'); endTT 参数调整要跟着问题走。maxRank先按 8、16、32 三档跑,观察残差和策略变化;ttTol从 1e-6 收到 1e-10,看 rank 涨不涨。牛顿外层建议保留damping和maxNewton两个旋钮,残差震荡先降阻尼,收敛慢再适当加。验证方法我习惯做三层:一维对比满网格 DP,二维对比 TT 展开后的满张量,高维只看 Bellman 残差和策略边界命中率。每层都留一个回归脚本,改完动力学跑一遍,比肉眼看图可靠。从那以后我每次换动力学都先跑一维断言和二维切片,确认尺寸、边界、残差三件事都过,再推高维。希望帮到你。
本文还有配套的精品资源,点击获取