實(shí)現(xiàn)與自動(dòng)調(diào)參工程實(shí)踐)
簡(jiǎn)介面向需要在Simulink環(huán)境中調(diào)試S函數(shù)參數(shù)的MATLAB學(xué)習(xí)者與工程人員這份資料圍繞M文件驅(qū)動(dòng)的自動(dòng)調(diào)參展開(kāi)可用于減少手工反復(fù)修改參數(shù)的繁瑣流程適合具備一定Simulink建模與腳本基礎(chǔ)的中級(jí)用戶。壓縮包共4個(gè)文件由3個(gè)m腳本和1個(gè)slx模型組成整體約24KB腳本承擔(dān)參數(shù)掃描、仿真控制與結(jié)果篩選等任務(wù)模型文件提供可運(yùn)行的S函數(shù)仿真載體。已有3819人學(xué)習(xí)下載說(shuō)明該主題在控制系統(tǒng)仿真與算法驗(yàn)證中具有一定關(guān)注度。讀者可據(jù)此理解自動(dòng)調(diào)參的組織方式復(fù)用腳本框架完成批量仿真、參數(shù)對(duì)比與較優(yōu)參數(shù)選取并借助配套模型快速驗(yàn)證思路降低重復(fù)搭建與調(diào)試成本。1. 從一次通宵調(diào)參說(shuō)起S 函數(shù)參數(shù)化能省掉什么某開(kāi)發(fā)者接了個(gè)控制回路整定的活被控對(duì)象和控制器都封在 Simulink 模型里控制器本體是一個(gè) S 函數(shù)真正要?jiǎng)拥闹挥?Kp、Ki 兩個(gè)增益。他一開(kāi)始靠手改對(duì)話框、點(diǎn)一次運(yùn)行、看一眼示波器改到第八版時(shí)已經(jīng)分不清哪條曲線對(duì)應(yīng)哪組數(shù)了。后來(lái)他把 S 函數(shù)換成 M 文件實(shí)現(xiàn)參數(shù)從模塊對(duì)話框讀再寫一個(gè)腳本循環(huán)喂參數(shù)、跑仿真、算誤差積分一晚上掃完兩百多組第二天拿最優(yōu)值收工。這套「MATLAB 里用 M 文件寫 Simulink 的 S 函數(shù)再掛一層自動(dòng)調(diào)參腳本」的組合適合控制邏輯已經(jīng)沉淀在 S 函數(shù)里、又不想靠手感試參數(shù)的人也適合剛接觸 Level-2 M 文件 S 函數(shù)、想弄清它和外部腳本怎么配合的讀者。下面按工程骨架、回調(diào)實(shí)現(xiàn)、調(diào)參引擎、踩坑排查、二次開(kāi)發(fā)幾段拆開(kāi)講。2. 工程骨架M 文件 S 函數(shù)、調(diào)參腳本與結(jié)果目錄怎么擺2.1 目錄結(jié)構(gòu)與文件職責(zé)這類項(xiàng)目最怕的是所有東西堆在一個(gè)文件夾里模型、S 函數(shù)、臨時(shí)腳本、結(jié)果數(shù)據(jù)混在一起跑上十幾輪以后自己都找不到哪個(gè) .mat 是哪次實(shí)驗(yàn)的。我一般按「模型 / 被調(diào)對(duì)象 / 調(diào)參邏輯 / 結(jié)果」四層拆目錄名盡量短方便addpath之后一眼看出哪個(gè)文件歸哪一層。sfunc_tune/ ├─ init_project.m % 統(tǒng)一入口加路徑、清緩存、設(shè)默認(rèn)參數(shù) ├─ models/ │ └─ loop_model.slx % 頂層模型里面放 S-Function 模塊 ├─ sfunc/ │ └─ msfcn_pid.m % Level-2 M 文件 S 函數(shù)本體 ├─ tune/ │ ├─ tune_main.m % 調(diào)參主流程網(wǎng)格 局部搜索 │ ├─ cost_once.m % 目標(biāo)函數(shù)一次仿真返回一個(gè)標(biāo)量代價(jià) │ ├─ build_grid.m % 生成候選參數(shù)組合 │ └─ save_result.m % 結(jié)果落盤帶時(shí)間戳 └─ results/ ├─ run_20240101_0930.mat └─ run_20240101_0930.csvinit_project.m里干三件事把sfunc和tune加進(jìn)搜索路徑檢查loop_model.slx能不能load_system清掉上一次可能殘留的基礎(chǔ)工作區(qū)變量。第三件事看著多余實(shí)際能避免很多「明明改了參數(shù)結(jié)果沒(méi)變」的假象——變量名撞上舊值仿真照樣跑只是跑的是舊參數(shù)。function init_project() % 工程統(tǒng)一入口路徑、模型、殘留變量三件事 root fileparts(mfilename(fullpath)); addpath(fullfile(root, sfunc)); addpath(fullfile(root, tune)); mdl loop_model; if ~bdIsLoaded(mdl) load_system(fullfile(root, models, [mdl .slx])); end % 清掉可能被上一次實(shí)驗(yàn)寫臟的變量 vars {Kp, Ki, simOut, yout}; for k 1:numel(vars) if evalin(base, [exist( vars{k} , var)]) evalin(base, [clear vars{k}]); end end fprintf([init] 工程已就緒模型%s\n, mdl); endmfilename(fullpath)拿的是當(dāng)前文件所在目錄這樣整個(gè)工程搬到別的機(jī)器上不用改路徑。bdIsLoaded判斷模型是否已經(jīng)在內(nèi)存里避免重復(fù)load_system報(bào)警告。最后那段循環(huán)用evalin(base, ...)而不是直接clear是因?yàn)楹瘮?shù)里的工作區(qū)和基礎(chǔ)工作區(qū)是兩回事S 函數(shù)讀的是基礎(chǔ)工作區(qū)里的變量清錯(cuò)地方等于沒(méi)清。2.2 一條參數(shù)從腳本到 S 函數(shù)的完整鏈路很多人卡在第一步腳本里算出來(lái)的參數(shù)怎么讓 Simulink 里的 S 函數(shù)讀到。常見(jiàn)做法有三條路選哪條取決于你要不要保留「手動(dòng)改參數(shù)」的能力。第一條是直接把數(shù)值寫進(jìn) S-Function 模塊的對(duì)話框參數(shù)用set_param改。好處是自包含壞處是每改一次都要?jiǎng)幽P头磸?fù)set_param會(huì)讓模型變臟跑完還得決定要不要保存。第二條是模塊對(duì)話框里填變量名比如Kp、Ki腳本往基礎(chǔ)工作區(qū)里寫同名變量。這條路最靈活同一個(gè)模型既能手動(dòng)跑也能腳本跑缺點(diǎn)是變量泄漏跑完不清理會(huì)污染下一輪。第三條是用Simulink.SimulationInput的setVariable把變量綁定到這一次仿真上作用域干凈還能配合parsim并行。我一般首選這條只有在需要兼容老版本模型時(shí)才退回第二條。完整鏈路是這樣的tune_main生成候選參數(shù) →cost_once用SimulationInput綁定Kp/Ki→sim跑一次 → 從輸出對(duì)象里取響應(yīng) → 算出代價(jià) → 返回給搜索器 → 記錄一行結(jié)果。S 函數(shù)那一端只認(rèn)模塊對(duì)話框里的兩個(gè)變量名完全不知道外面在掃參。2.3 為什么用 Level-2 M 文件 S 函數(shù)而不是 C-MEX標(biāo)題里點(diǎn)的是 M 文件實(shí)現(xiàn)這里得說(shuō)清楚取舍。Level-2 M 文件 S 函數(shù)是解釋執(zhí)行的每個(gè)仿真步都要回調(diào)一次 MATLAB 函數(shù)步長(zhǎng)小、仿真時(shí)間長(zhǎng)的時(shí)候慢得明顯比 C-MEX 版本慢一個(gè)數(shù)量級(jí)不夸張。自動(dòng)調(diào)參要跑幾十上百次仿真這個(gè)開(kāi)銷會(huì)直接翻倍。但它有三個(gè)好處在調(diào)參場(chǎng)景里權(quán)重很高。一是不用mex編譯改完公式保存就能跑調(diào)參過(guò)程中你隨時(shí)可能想改控制律C 文件編譯一遍再鏈接的反饋循環(huán)太長(zhǎng)。二是出錯(cuò)信息是 MATLAB 的堆棧哪一行越界、哪個(gè)維度對(duì)不上點(diǎn)進(jìn)去就能看不像 MEX 那樣只剩一句籠統(tǒng)的崩潰。三是它能直接調(diào)用工作區(qū)里的函數(shù)和參數(shù)結(jié)構(gòu)體寫代價(jià)函數(shù)、寫輔助邏輯不用重復(fù)實(shí)現(xiàn)。所以我的判斷標(biāo)準(zhǔn)很粗暴算法還在試、參數(shù)還在掃用 M 文件版算法定型、要交給別人跑長(zhǎng)仿真再翻譯成 C-MEX 或者改成 MATLAB Function 塊。中間不要來(lái)回折騰先讓參數(shù)定下來(lái)。提示msfcn_開(kāi)頭的函數(shù)名不是強(qiáng)制的但約定俗成sfundemos里的例子基本都是這個(gè)前綴排查問(wèn)題時(shí)搜起來(lái)方便。3. S 函數(shù)本體拆解六個(gè)回調(diào)里哪些和調(diào)參直接相關(guān)3.1 setup 回調(diào)參數(shù)個(gè)數(shù)、采樣時(shí)間與狀態(tài)維度Level-2 M 文件 S 函數(shù)的結(jié)構(gòu)是固定的主函數(shù)同名里面調(diào)setupsetup負(fù)責(zé)聲明這個(gè)塊的輸入輸出、參數(shù)個(gè)數(shù)、狀態(tài)個(gè)數(shù)然后用RegBlockMethod把各個(gè)回調(diào)注冊(cè)進(jìn)去。自動(dòng)調(diào)參要?jiǎng)拥膮?shù)全部在NumDialogPrms里聲明。function msfcn_pid(block) % Level-2 M 文件 S 函數(shù)入口名字必須和文件名一致 setup(block); end function setup(block) % 兩個(gè)可調(diào)參數(shù)Kp、Ki一個(gè)誤差輸入一個(gè)控制輸出 block.NumDialogPrms 2; block.NumInputPorts 1; block.NumOutputPorts 1; % 輸入端口標(biāo)量誤差直通 block.InputPort(1).Dimensions 1; block.InputPort(1).DatatypeID 0; % double block.InputPort(1).Complexity Real; block.InputPort(1).DirectFeedthrough true; % 輸出依賴當(dāng)前輸入 % 輸出端口標(biāo)量控制量 block.OutputPort(1).Dimensions 1; block.OutputPort(1).DatatypeID 0; block.OutputPort(1).Complexity Real; % 一個(gè)連續(xù)狀態(tài)積分項(xiàng) block.NumContStates 1; block.SampleTimes [-1 0]; % 繼承上游采樣時(shí)間 % 注冊(cè)回調(diào)注意順序無(wú)所謂但名字必須對(duì) block.RegBlockMethod(Start, Start); block.RegBlockMethod(Outputs, Outputs); block.RegBlockMethod(Derivatives, Derivatives); block.RegBlockMethod(Terminate, Terminate); endNumDialogPrms 2決定了模塊對(duì)話框里會(huì)出現(xiàn)兩個(gè)參數(shù)框多一個(gè)少一個(gè)都會(huì)在編譯階段報(bào)參數(shù)數(shù)量不匹配。DirectFeedthrough true是因?yàn)檩敵鲋苯佑昧水?dāng)前步的輸入誤差如果你的 S 函數(shù)輸出只依賴狀態(tài)這里要設(shè)成false否則會(huì)憑空造出代數(shù)環(huán)。SampleTimes [-1 0]表示繼承采樣時(shí)間調(diào)參時(shí)上游信號(hào)源的步長(zhǎng)改了S 函數(shù)跟著變不用同步改兩處。連續(xù)狀態(tài)個(gè)數(shù)寫 1對(duì)應(yīng)積分項(xiàng)這個(gè)數(shù)字和后面Derivatives里賦值的維度必須一致。3.2 Outputs 與 Update把可調(diào)參數(shù)真正用起來(lái)Outputs每個(gè)仿真步都會(huì)被調(diào)用是控制律真正生效的地方。參數(shù)從block.DialogPrm(k).Data取第 k 個(gè)對(duì)應(yīng)對(duì)話框里第 k 個(gè)框索引從 1 開(kāi)始這里最容易寫錯(cuò)。function Outputs(block) % 每個(gè)仿真步計(jì)算控制量u Kp*e xi Kp block.DialogPrm(1).Data; Ki block.DialogPrm(2).Data; e block.InputPort(1).Data; xi block.ContStates.Data(1); u Kp * e Ki * xi; block.OutputPort(1).Data u; end function Derivatives(block) % 積分項(xiàng)導(dǎo)數(shù)dxi/dt e e block.InputPort(1).Data; block.Derivatives(1).Data e; end這里把積分增益放在Outputs里乘而不是在Derivatives里乘是為了讓 Kp、Ki 兩個(gè)參數(shù)在同一個(gè)函數(shù)里出現(xiàn)改公式的時(shí)候不容易漏。block.ContStates.Data(1)讀當(dāng)前狀態(tài)值block.Derivatives(1).Data寫導(dǎo)數(shù)兩者維度都必須是 1寫錯(cuò)會(huì)報(bào)「derivative dimension mismatch」。參數(shù)讀取用DialogPrm(k).Data而不是DialogPrm(k)少寫.Data拿到的是結(jié)構(gòu)體參與運(yùn)算會(huì)直接報(bào)類型錯(cuò)誤。如果你想在仿真過(guò)程中打印參數(shù)確認(rèn)腳本確實(shí)傳進(jìn)來(lái)了別用disp——每個(gè)步長(zhǎng)打一行會(huì)把命令行刷爆。用persistent記一個(gè)首次標(biāo)志只在第一步打印一次function Outputs(block) persistent printed if isempty(printed) fprintf([sfunc] Kp%.4f, Ki%.4f\n, ... block.DialogPrm(1).Data, block.DialogPrm(2).Data); printed true; end % ... 后續(xù)控制律 endpersistent變量在兩次sim之間不會(huì)自動(dòng)清空這一點(diǎn)在調(diào)參場(chǎng)景里是個(gè)隱患后面避坑章節(jié)會(huì)專門講。3.3 Start 與 Terminate每次仿真前的狀態(tài)復(fù)位自動(dòng)調(diào)參要跑幾十次仿真每次都必須是干凈起點(diǎn)否則第二輪繼承第一輪的積分器殘值結(jié)果沒(méi)有可比性。Start回調(diào)就是干這個(gè)的。function Start(block) % 每次仿真開(kāi)始把連續(xù)狀態(tài)清零 block.ContStates.Data 0; end function Terminate(block) % 仿真結(jié)束時(shí)的收尾記錄、清理、關(guān)文件 fprintf([sfunc] 本次仿真結(jié)束\n); endStart里給ContStates.Data賦初值等價(jià)于在連續(xù)系統(tǒng)里設(shè)初始條件。不寫這一句Simulink 會(huì)用上一次的值或者默認(rèn)零短仿真看不出差別長(zhǎng)仿真和多次連續(xù)調(diào)用時(shí)會(huì)出現(xiàn)莫名其妙的漂移。Terminate里不要做重活打印一行、關(guān)一個(gè)文件句柄就夠了它會(huì)在每次仿真結(jié)束時(shí)被調(diào)用寫太多會(huì)拖慢整體掃參速度。有人喜歡在Start里用assignin往外寫變量做記錄這個(gè)習(xí)慣要克制。S 函數(shù)執(zhí)行在仿真引擎內(nèi)部往里塞副作用容易和并行仿真沖突記錄的事情交給外層的cost_once更穩(wěn)妥。3.4 用 block 參數(shù)做在線可調(diào)的入口除了對(duì)話框參數(shù)Level-2 S 函數(shù)還支持運(yùn)行時(shí)參數(shù)Run-time Parameter注冊(cè)之后可以在仿真運(yùn)行過(guò)程中用set_param改不用重啟仿真。做參數(shù)掃描時(shí)用不上但做「參數(shù)自適應(yīng)」或者「手動(dòng)拖滑塊看響應(yīng)」的場(chǎng)景很有用。function setup(block) block.NumDialogPrms 2; % 把第一個(gè)參數(shù)注冊(cè)成運(yùn)行時(shí)參數(shù)可在仿真中修改 block.RegBlockMethod(PostPropagationSetup, DoPostPropSetup); end function DoPostPropSetup(block) block.NumDworks 1; block.Dwork(1).Name KpRt; block.Dwork(1).Dimensions 1; block.Dwork(1).DatatypeID 0; block.Dwork(1).Complexity Real; block.Dwork(1).UsedAsDiscState true; end這段是給你留個(gè)口子真要做在線調(diào)參把Kp從對(duì)話框搬到運(yùn)行時(shí)參數(shù)外部用set_param在仿真中途改值就能看到響應(yīng)實(shí)時(shí)變化。代價(jià)是代碼復(fù)雜度上去一截而且運(yùn)行時(shí)參數(shù)在代碼生成里支持有限。我一般只在演示或者交互式調(diào)試時(shí)用批量掃參還是走對(duì)話框參數(shù)那條路邏輯簡(jiǎn)單、可復(fù)現(xiàn)。4. 自動(dòng)調(diào)參引擎把 sim 命令包進(jìn)一個(gè)可迭代的目標(biāo)函數(shù)4.1 目標(biāo)函數(shù)一次仿真返回一個(gè)標(biāo)量代價(jià)搜索器只認(rèn)一個(gè)東西輸入一組參數(shù)輸出一個(gè)越小越好的標(biāo)量。所以第一件事是把「跑仿真 算指標(biāo)」封成一個(gè)函數(shù)中間過(guò)程全部藏在里面。function J cost_once(p, cfg) % p : 候選參數(shù) [Kp, Ki] % cfg : 配置結(jié)構(gòu)體含模型名、仿真時(shí)長(zhǎng)、上下界、懲罰值 % J : 標(biāo)量代價(jià)越小越好 % 越界直接給懲罰不去跑仿真省時(shí)間 if any(p cfg.lb) || any(p cfg.ub) J cfg.penalty; return; end in Simulink.SimulationInput(cfg.model); in in.setVariable(Kp, p(1)); in in.setVariable(Ki, p(2)); in in.setModelParameter(StopTime, num2str(cfg.tStop)); in in.setModelParameter(SimulationMode, normal); in in.setModelParameter(SaveFormat, Structure with time); in in.setModelParameter(SaveOutput, on); in in.setModelParameter(OutputSaveName, yout); try out sim(in); catch ME % 仿真炸了也給懲罰但把原因記下來(lái) fprintf([cost] 仿真失敗%s\n, ME.message); J cfg.penalty; return; end y out.get(yout); t y.time; sig y.signals.values; ref cfg.ref; % 參考信號(hào)標(biāo)量或同長(zhǎng)度向量 e ref - sig; J trapz(t, abs(e)); % IAE 指標(biāo) J J cfg.wOvershoot * max(0, max(sig) - max(ref)); % 超調(diào)懲罰 endsetVariable綁定的變量只在這一次仿真里有效跑完自動(dòng)釋放不會(huì)污染基礎(chǔ)工作區(qū)這是它比assignin干凈的地方。SimulationMode必須顯式設(shè)成normalM 文件 S 函數(shù)在加速模式下不支持不設(shè)的話模型上次保存成 Accelerator 就會(huì)直接報(bào)錯(cuò)。try/catch里返回懲罰值而不是拋出去是因?yàn)樗阉髌饔龅揭淮问【椭袛嗵速M(fèi)給個(gè)大代價(jià)讓它自己繞開(kāi)這組參數(shù)更劃算。指標(biāo)用trapz算 IAE比簡(jiǎn)單求和更貼近連續(xù)系統(tǒng)的定義。超調(diào)懲罰單獨(dú)加權(quán)權(quán)重wOvershoot放在配置里方便你在「快但抖」和「慢但穩(wěn)」之間調(diào)傾向。4.2 主循環(huán)網(wǎng)格掃描與局部搜索的組合純網(wǎng)格掃得全但慢純梯度搜索快但容易掉進(jìn)局部極小??刂茀?shù)這類問(wèn)題我一般先用粗網(wǎng)格框出大致區(qū)域再用fminsearch在局部細(xì)化兩段式比單用一種穩(wěn)。function best tune_main() cfg struct(model, loop_model, tStop, 10, ref, 1, ... lb, [0 0], ub, [10 3], penalty, 1e6, ... wOvershoot, 2); KpGrid linspace(0.2, 5.0, 8); KiGrid linspace(0.0, 2.0, 6); records zeros(numel(KpGrid) * numel(KiGrid), 3); % Kp, Ki, J idx 0; best struct(J, inf, p, [NaN NaN]); for kp KpGrid for ki KiGrid idx idx 1; p [kp ki]; J cost_once(p, cfg); records(idx, :) [kp ki J]; if J best.J best.J J; best.p p; fprintf([grid] 新最優(yōu) J%.4f Kp%.3f, Ki%.3f\n, J, kp, ki); end end end % 局部細(xì)化從網(wǎng)格最優(yōu)點(diǎn)出發(fā) opts optimset(Display, iter, TolX, 1e-3, MaxIter, 40); pFine fminsearch((p) cost_once(p, cfg), best.p, opts); JFine cost_once(pFine, cfg); if JFine best.J best.p pFine; best.J JFine; end save_result(records, best, cfg); end網(wǎng)格那段是雙循環(huán)每個(gè)組合調(diào)一次cost_oncerecords矩陣把三元組存下來(lái)最后一起落盤。fminsearch從網(wǎng)格最優(yōu)點(diǎn)出發(fā)TolX控制參數(shù)收斂精度MaxIter防止它在一個(gè)平坦區(qū)域里磨太久。注意fminsearch本身不管邊界越界的情況靠cost_once開(kāi)頭的懲罰兜住——這也是為什么懲罰值要給得足夠大太小的話搜索器會(huì)心安理得地待在界外。4.3 接入 fminsearch / ga 的寫法與邊界約束fminsearch夠用但它對(duì)初值敏感、不支持約束。參數(shù)維度上去了、或者你明確知道 Kp 和 Ki 有耦合關(guān)系換遺傳算法或者模式搜索更合適。接口是一模一樣的都是「輸入向量、輸出標(biāo)量」。% 方案一模式搜索自帶邊界約束 opts optimoptions(patternsearch, Display, iter, ... MaxIterations, 60, MeshTolerance, 1e-3); [pOpt, JOpt] patternsearch((p) cost_once(p, cfg), best.p, ... [], [], [], [], cfg.lb, cfg.ub, [], opts); % 方案二遺傳算法適合多峰、參數(shù)多于 3 個(gè)的情況 opts optimoptions(ga, Display, iter, PopulationSize, 20, ... MaxGenerations, 30, UseParallel, false); [pOpt, JOpt] ga((p) cost_once(p, cfg), 2, ... [], [], [], [], cfg.lb, cfg.ub, [], opts);patternsearch的好處是邊界直接作為參數(shù)傳進(jìn)去不用靠懲罰收斂過(guò)程更干凈代價(jià)是每輪要評(píng)估的點(diǎn)數(shù)比f(wàn)minsearch多。ga適合參數(shù)多、代價(jià)函數(shù)有多個(gè)局部極小的情況但它的隨機(jī)性意味著兩次跑出來(lái)的結(jié)果可能不一樣做實(shí)驗(yàn)記錄的時(shí)候要把隨機(jī)種子固定下來(lái)否則復(fù)現(xiàn)不了。UseParallel那一項(xiàng)先關(guān)掉等單機(jī)流程跑通了再打開(kāi)不然出錯(cuò)時(shí)你分不清是并行的問(wèn)題還是代價(jià)函數(shù)的問(wèn)題。4.4 結(jié)果落盤與最優(yōu)參數(shù)回寫掃完一輪結(jié)果要能查、能對(duì)比、能復(fù)現(xiàn)。save_result干三件事存.mat方便下次直接load寫.csv方便丟進(jìn)表格里看趨勢(shì)把最優(yōu)參數(shù)寫回基礎(chǔ)工作區(qū)方便立刻手動(dòng)驗(yàn)證。function save_result(records, best, cfg) stamp datestr(now, yyyymmdd_HHMM); matFile fullfile(results, [run_ stamp .mat]); csvFile fullfile(results, [run_ stamp .csv]); save(matFile, records, best, cfg); fid fopen(csvFile, w); fprintf(fid, Kp,Ki,J\n); for k 1:size(records, 1) fprintf(fid, %.6f,%.6f,%.6f\n, records(k, 1), records(k, 2), records(k, 3)); end fclose(fid); % 回寫最優(yōu)值方便立刻手動(dòng)跑一次確認(rèn) assignin(base, Kp, best.p(1)); assignin(base, Ki, best.p(2)); fprintf([save] 最優(yōu) J%.4f, Kp%.4f, Ki%.4f\n, best.J, best.p(1), best.p(2)); end文件名帶時(shí)間戳跑一百輪也不會(huì)互相覆蓋。.mat里存了cfg下次想復(fù)現(xiàn)同一組實(shí)驗(yàn)把cfg取出來(lái)直接用不用回憶當(dāng)時(shí)設(shè)的仿真時(shí)長(zhǎng)和懲罰權(quán)重。最后那兩行assignin是給手動(dòng)驗(yàn)證留的口子模型對(duì)話框里填的是變量名Kp、Ki腳本跑完工作區(qū)里就是最優(yōu)值打開(kāi)模型點(diǎn)一次運(yùn)行就能看響應(yīng)。5. 避坑與排查五類讓調(diào)參結(jié)果不可信的坑5.1 參數(shù)個(gè)數(shù)不匹配S 函數(shù)在編譯階段就掛現(xiàn)象很直接點(diǎn)運(yùn)行命令行報(bào)S-function msfcn_pid has fewer parameters in its dialog than it expects或者反過(guò)來(lái)多了一個(gè)。原因通常是setup里NumDialogPrms改了但模型里那個(gè) S-Function 模塊的對(duì)話框沒(méi)同步改兩個(gè)地方各說(shuō)各話。解決方法是把「聲明」和「實(shí)例」當(dāng)成一對(duì)強(qiáng)耦合的東西來(lái)維護(hù)。改NumDialogPrms之后一定打開(kāi)模型確認(rèn)參數(shù)框數(shù)量和順序順序錯(cuò)了不會(huì)報(bào)錯(cuò)但 Kp 會(huì)讀到 Ki 的值跑出來(lái)的結(jié)果看著像那么回事其實(shí)全錯(cuò)。我一般會(huì)在setup里加一段自檢參數(shù)個(gè)數(shù)不對(duì)時(shí)直接error并打印期望值比等到仿真跑一半再發(fā)現(xiàn)強(qiáng)。function setup(block) block.NumDialogPrms 2; if numel(block.DialogPrm) ~ 2 error(msfcn_pid:prmCount, ... 期望 2 個(gè)參數(shù)實(shí)際收到 %d 個(gè), numel(block.DialogPrm)); end end5.2 加速模式與 M 文件 S 函數(shù)不兼容現(xiàn)象是仿真剛啟動(dòng)就報(bào)M-file S-function cannot be used in accelerated mode或者切到 Accelerator 之后模型直接編譯失敗。原因是 Level-2 M 文件 S 函數(shù)是解釋執(zhí)行的加速模式要把它編譯成目標(biāo)代碼兩者天然沖突。解決方式只有兩條要么在調(diào)參腳本里強(qiáng)制SimulationMode為normal要么把邏輯改寫成 MATLAB Function 塊或 C-MEX。前一條是調(diào)參階段的默認(rèn)選擇寫進(jìn)cost_once里就不會(huì)忘。真正要跑長(zhǎng)仿真、要加速的時(shí)候說(shuō)明參數(shù)已經(jīng)定得差不多了那時(shí)候再做移植順序不要反。5.3 狀態(tài)未復(fù)位第二輪結(jié)果開(kāi)始漂移這個(gè)坑最陰。第一輪仿真結(jié)果正常從第二輪開(kāi)始響應(yīng)曲線整體偏移一點(diǎn)越往后偏得越多但腳本、參數(shù)、模型一個(gè)都沒(méi)改。原因有兩類一類是Start里沒(méi)給ContStates.Data賦初值積分器繼承了上一輪的值另一類是 S 函數(shù)里用了persistent變量做計(jì)數(shù)或緩存兩次sim之間不會(huì)被清空。排查方法是把同一組參數(shù)連續(xù)跑三次比較三次的輸出是否逐位相同。不相同就順著persistent關(guān)鍵字和ContStates找。解決上狀態(tài)一律在Start里顯式清零persistent能不用就不用非要用就在Start里重置function Start(block) block.ContStates.Data 0; clear msfcn_pid % 清掉本函數(shù)內(nèi)的 persistent end5.4 數(shù)據(jù)記錄格式選錯(cuò)指標(biāo)算出來(lái)是錯(cuò)的現(xiàn)象是從out里取出來(lái)的yout維度不對(duì)signals.values是雙列或者時(shí)間向量長(zhǎng)度和信號(hào)長(zhǎng)度對(duì)不上trapz算出來(lái)的 IAE 數(shù)量級(jí)明顯離譜。原因是SaveFormat設(shè)成了Dataset或者Array讀取方式和Structure with time完全不同。兩種讀法要分清楚Structure with time用y.time和y.signals.valuesDataset用y.get(信號(hào)名).Values.Data和.Time。調(diào)參腳本里統(tǒng)一用一種別一半腳本用這種一半用那種。我習(xí)慣在cost_once開(kāi)頭就把SaveFormat釘死讀的時(shí)候加一句維度斷言assert(size(sig, 2) 1, yout 維度異常檢查 SaveFormat 設(shè)置);5.5 路徑與命名沖突現(xiàn)象是Undefined function msfcn_pid或者改完文件內(nèi)容運(yùn)行結(jié)果不變。原因通常有三個(gè)函數(shù)名和文件名不一致Level-2 M 文件 S 函數(shù)要求嚴(yán)格同名文件所在目錄沒(méi)加進(jìn)搜索路徑工作區(qū)里存在同名變量遮蔽了函數(shù)。排查順序是which msfcn_pid -all看有幾個(gè)同名文件、path看目錄在不在、whos msfcn_pid看有沒(méi)有變量撞名。這三個(gè)命令跑一遍九成問(wèn)題當(dāng)場(chǎng)現(xiàn)形。工程里我固定用init_project統(tǒng)一加路徑就是為了把這類問(wèn)題擋在入口。6. 二次開(kāi)發(fā)把單機(jī)掃參改成可復(fù)用的調(diào)參流水線單機(jī)腳本跑通之后最值得做的改造是并行。cost_once之間互不依賴把網(wǎng)格那層雙循環(huán)換成parfor或者直接用parsim批量提交SimulationInput數(shù)組四核機(jī)器上差不多能壓到四分之一時(shí)間。改的時(shí)候注意兩件事cost_once里不能有對(duì)基礎(chǔ)工作區(qū)的寫操作SimulationInput要提前構(gòu)造成數(shù)組再一次性提交否則并行收益會(huì)被調(diào)度開(kāi)銷吃掉。第二個(gè)改造方向是把代價(jià)函數(shù)從單目標(biāo)擴(kuò)成多目標(biāo)比如同時(shí)看調(diào)節(jié)時(shí)間和超調(diào)量用加權(quán)和或者 Pareto 前沿。加權(quán)系數(shù)放在cfg里別硬編碼在函數(shù)內(nèi)部否則每換一個(gè)工況就要改代碼。第三個(gè)方向是把最優(yōu)參數(shù)自動(dòng)回寫到模塊的 mask 或者數(shù)據(jù)字典里形成「掃參—定參—固化」的閉環(huán)。這一步我建議保留人工確認(rèn)環(huán)節(jié)腳本只負(fù)責(zé)把候選值和對(duì)應(yīng)的響應(yīng)曲線存成圖最終寫不寫回由人拍板。自動(dòng)寫回看著省事一旦代價(jià)函數(shù)設(shè)計(jì)得有偏固化的就是一組錯(cuò)參數(shù)。驗(yàn)證方法上我固定做三件事拿最優(yōu)參數(shù)手動(dòng)跑一次和腳本記錄的最優(yōu)代價(jià)對(duì)比誤差超過(guò)百分之一就說(shuō)明記錄鏈路有問(wèn)題換一組沒(méi)參與搜索的工況參數(shù)跑一次看泛化把隨機(jī)種子固定下來(lái)重復(fù)跑一遍結(jié)果應(yīng)當(dāng)逐位一致不一致就回去查persistent和狀態(tài)復(fù)位。有一回我圖省事把cost_once里的SimulationMode那行刪了想著模型默認(rèn)就是 normal結(jié)果模型被同事存成了 Accelerator一整晚的掃參全部作廢最優(yōu)值是一組隨機(jī)數(shù)。從那以后我每次開(kāi)新工程都強(qiáng)制先跑一遍init_project加三次重復(fù)性自檢確認(rèn)三次輸出完全一致再開(kāi)始掃參。希望這套流程能幫到你少走一遍我踩過(guò)的彎路。本文還有配套的精品資源點(diǎn)擊獲取