From 57ba7b5feefa8647e3ac3aad734d9b5a1b909659 Mon Sep 17 00:00:00 2001 From: HY <3258556096@qq.com> Date: Tue, 7 Apr 2026 01:25:46 +0800 Subject: [PATCH 1/3] feat: add case demo module and streamline Model assets --- CHANGELOG.md | 16 + GUIDANCE.md | 46 +- Model/data/Cleaned_Engine_Data_Full.csv | 785 +++++++++++++++ Model/data/engine_gpr_model.pth | Bin 0 -> 6457 bytes Model/src/battery_sim.py | 168 ++++ Model/src/engine_dynamic_sim.py | 358 +++++++ Model/src/engine_gpr_class.py | 283 ++++++ Model/src/increPID.py | 88 ++ Model/src/motor_sim.py | 1225 +++++++++++++++++++++++ Model/src/series_hybrid_sim.py | 261 +++++ README.md | 61 +- app.py | 43 +- case_demo_functions.py | 145 +++ config.py | 2 +- data_usage/case_demo_test_results.json | 70 ++ data_usage/usage_stats.json | 4 +- requirements.txt | 26 +- ui_components.py | 31 + 18 files changed, 3569 insertions(+), 43 deletions(-) create mode 100644 Model/data/Cleaned_Engine_Data_Full.csv create mode 100644 Model/data/engine_gpr_model.pth create mode 100644 Model/src/battery_sim.py create mode 100644 Model/src/engine_dynamic_sim.py create mode 100644 Model/src/engine_gpr_class.py create mode 100644 Model/src/increPID.py create mode 100644 Model/src/motor_sim.py create mode 100644 Model/src/series_hybrid_sim.py create mode 100644 case_demo_functions.py create mode 100644 data_usage/case_demo_test_results.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 1fa6f77..aa38048 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-04-07 + +### Added +- ✨ 新增“算例演示(Case Demo)”标签页,位于根轨迹与智能问答之间 +- ✨ 新增 `case_demo_functions.py`,支持参数化工况运行完整混动模型 +- ✨ 新增图文输出:转速响应、功率分配、电气状态、关键时刻数据表 +- ✨ 新增算例结果自动解读(最大转速误差、SOC变化、平均功率、平均燃油流量) + +### Changed +- 🔧 `Model/src/series_hybrid_sim.py` 改为按 Model 目录定位 GPR 数据与权重 +- 🔧 `requirements.txt` 固定关键依赖版本并加入 PyTorch CPU 下载源 +- 🔧 文档更新:README 与 GUIDANCE 同步为“五大功能”结构与最新运行方式 + +### Removed +- 🧹 精简 `Model` 目录:移除 `scripts/`、`EngineData.xlsx`、`.git/`、`.claude/`、`README.md`、`LICENSE`、`environment.yml`、`figures/`、`.gitignore` + ## [1.0.0] - 2025-10-15 ### Added diff --git a/GUIDANCE.md b/GUIDANCE.md index dd1e7dd..f83ccd4 100644 --- a/GUIDANCE.md +++ b/GUIDANCE.md @@ -10,12 +10,13 @@ ## 二、平台功能简介 -本平台是《自动控制理论》课程的配套学习工具,提供四大核心功能: +本平台是《自动控制理论》课程的配套学习工具,提供五大核心功能: 1. **时域分析** - 分析系统的阶跃响应和脉冲响应 2. **频域分析** - 绘制Bode图和Nyquist图,分析系统稳定性 3. **根轨迹分析** - 观察增益变化对系统极点的影响 -4. **AI智能问答** - 24小时在线的自动控制理论助教 +4. **算例演示** - 基于完整混动发动机模型的参数化仿真 +5. **AI智能问答** - 24小时在线的自动控制理论助教 --- @@ -107,7 +108,38 @@ --- -### 3.4 AI智能问答 +### 3.4 算例演示 + +#### 3.4.1 功能说明 + +通过完整混动模型进行系统级算例仿真,输出图文结果用于教学演示与参数对比。 + +#### 3.4.2 使用步骤 + +1. 点击顶部"算例演示"标签页 +2. 设置工况模板与参数: + - 仿真时长、仿真步长 + - 初始SOC、初始发动机功率 + - 目标转速缩放系数、负载转矩缩放系数 +3. 点击"运行混动算例" +4. 查看三联图、结果解读与关键时刻数据表 + +#### 3.4.3 输出结果 + +- **推进轴转速响应图**:目标与实际转速跟踪 +- **功率分配图**:电机需求、发动机输出、电池功率 +- **电气状态图**:母线电压与SOC +- **结果解读**:最大转速误差、SOC变化、平均功率、平均燃油流量 + +#### 3.4.4 结果解释要点 + +- 仿真时长是模型时间,不等于程序实际等待时间 +- 电池功率正值表示放电,负值表示充电 +- 输入超限时会自动裁剪到安全范围 + +--- + +### 3.5 AI智能问答 #### 3.4.1 功能说明 @@ -268,12 +300,12 @@ ### 9.1 平台特色功能 1. **实时在线人数统计** - 页面顶部显示当前在线用户数 -2. **自动保存会话** - AI问答记录自动保存 +2. **图文联动演示** - 算例演示支持图、表、文本同步展示 3. **LaTeX公式支持** - 完美显示数学公式 4. **响应式设计** - 适配不同屏幕尺寸 ### 9.2 更新说明 -- 版本: v1.1.0 -- 更新日期: 2025年10月19日 -- 主要更新: 新增实时在线人数统计功能 +- 版本: v1.2.0 +- 更新日期: 2026年4月7日 +- 主要更新: 新增算例演示模块与完整混动模型联动 diff --git a/Model/data/Cleaned_Engine_Data_Full.csv b/Model/data/Cleaned_Engine_Data_Full.csv new file mode 100644 index 0000000..fab5d45 --- /dev/null +++ b/Model/data/Cleaned_Engine_Data_Full.csv @@ -0,0 +1,785 @@ +Altitude_m,Mach,RPM,SFC,Power_kW,Inlet_T2_K,Inlet_P2_kPa,RPM_Rel_Phys,RPM_Rel_Corr,Flight_Vel_kmh,WF_kg_h +0.0,0.0,58000.0,0.517840950638725,249.582776334593,288.15,101.325005740179,1.0,1.00000000810418,0.0,129.24418216015792 +0.0,0.0,56550.0,0.523671728560371,225.628264104881,288.15,101.325005740179,0.975,0.975000007901579,0.0,118.15514307587895 +0.0,0.0,55100.0,0.541853755600857,199.858261719303,288.15,101.325005740179,0.95,0.950000007698975,0.0,108.29394970046332 +0.0,0.0,53650.0,0.574210664480304,172.570200963337,288.15,101.325005740179,0.925,0.92500000749637,0.0,99.09164976465733 +0.0,0.0,52200.0,0.634153554768018,141.195238600974,288.15,101.325005740179,0.9,0.900000007293765,0.0,89.53946247512614 +0.0,0.0,50750.0,0.739625131431326,108.051321175276,288.15,101.325005740179,0.875,0.875000007091161,0.0,79.91747262559193 +0.0,0.0,49300.0,0.889708703850822,80.8082872194116,288.15,101.325005740179,0.85,0.850000006888556,0.0,71.89583648238764 +0.0,0.0,47850.0,1.08140792247008,60.7887493798357,288.15,101.325005740179,0.825,0.825000006685951,0.0,65.7374351764025 +0.0,0.0,46400.0,1.29064944286157,47.6405112445542,288.15,101.325005740179,0.8,0.800000006483347,0.0,61.48719929542423 +0.0,0.0,44950.0,1.51493801357613,38.5098458611298,288.15,101.325005740179,0.775,0.775000006280742,0.0,58.340029391982924 +0.0,0.0,43500.0,1.77665628493182,31.3519538053187,288.15,101.325005740179,0.75,0.750000006078138,0.0,55.70164577311156 +0.0,0.0,42050.0,2.13113424114961,24.9483532621358,288.15,101.325005740179,0.725,0.725000005875533,0.0,53.168289897234175 +0.0,0.0,40600.0,2.66079262801875,19.0435113641824,288.15,101.325005740179,0.7,0.700000005672928,0.0,50.67083464940782 +0.0,0.0,0.0,0.0,0.0,288.15,101.325005740179,0.0,0.0,0.0,0.0 +0.0,0.05,58000.0,0.516516781939578,250.658217899343,288.294288851519,101.50245395591,1.0,0.999749731014721,61.2540781071208,129.46917607607816 +0.0,0.05,56550.0,0.522545552740258,226.315592419349,288.294288851519,101.50245395591,0.975,0.974755987739353,61.2540781071208,118.26020633450767 +0.0,0.05,55100.0,0.540702165324945,200.446769067162,288.294288851519,101.50245395591,0.95,0.949762244463985,61.2540781071208,108.3820020670037 +0.0,0.05,53650.0,0.572924816015739,173.069984669624,288.294288851519,101.50245395591,0.925,0.924768501188617,61.2540781071208,99.15608912469109 +0.0,0.05,52200.0,0.632705856008601,141.575524377551,288.294288851519,101.50245395591,0.9,0.899774757913249,61.2540781071208,89.57566334116497 +0.0,0.05,50750.0,0.73790833583659,108.30563465016,288.294288851519,101.50245395591,0.875,0.874781014637881,61.2540781071208,79.91963062642529 +0.0,0.05,49300.0,0.887385695585721,81.0001237411043,288.294288851519,101.50245395591,0.85,0.849787271362513,61.2540781071208,71.87835114852932 +0.0,0.05,47850.0,1.07779232680311,60.9732715859449,288.294288851519,101.50245395591,0.825,0.824793528087145,61.2540781071208,65.7165242554135 +0.0,0.05,46400.0,1.28582871717267,47.7971346450755,288.294288851519,101.50245395591,0.8,0.799799784811776,61.2540781071208,61.45892832520682 +0.0,0.05,44950.0,1.50879846329932,38.6449856348878,288.294288851519,101.50245395591,0.775,0.774806041536408,61.2540781071208,58.307494940143016 +0.0,0.05,43500.0,1.76929731842248,31.4626795287867,288.294288851519,101.50245395591,0.75,0.74981229826104,61.2540781071208,55.66683452066817 +0.0,0.05,42050.0,2.12262473079863,25.0296488161928,288.294288851519,101.50245395591,0.725,0.724818554985672,61.2540781071208,53.12855158045549 +0.0,0.05,40600.0,2.65054955410193,19.0998400915557,288.294288851519,101.50245395591,0.7,0.699824811710304,61.2540781071208,50.62507263809112 +0.0,0.05,0.0,0.0,0.0,288.15,101.50245395591,0.0,0.0,0.0,0.0 +0.0,0.1,58000.0,0.513436195618546,252.835841959937,288.727155406076,102.036129697856,1.0,0.999000025738633,122.508156214242,129.815072811922 +0.0,0.1,56550.0,0.519207561513579,228.379823641232,288.727155406076,102.036129697856,0.975,0.974025025095167,122.508156214242,118.57653133166531 +0.0,0.1,55100.0,0.537279892817707,202.216306865266,288.727155406076,102.036129697856,0.95,0.949050024451701,122.508156214242,108.64675567856268 +0.0,0.1,53650.0,0.569111688484241,174.569497630313,288.727155406076,102.036129697856,0.925,0.924075023808235,122.508156214242,99.34954155423314 +0.0,0.1,52200.0,0.628416151322243,142.713704486095,288.727155406076,102.036129697856,0.9,0.899100023164769,122.508156214242,89.68359691409175 +0.0,0.1,50750.0,0.732800403291141,109.070720613081,288.727155406076,102.036129697856,0.875,0.874125022521303,122.508156214242,79.92706805252112 +0.0,0.1,49300.0,0.880428242025916,81.5850645288629,288.727155406076,102.036129697856,0.85,0.849150021877838,122.508156214242,71.82979493871768 +0.0,0.1,47850.0,1.06703252243469,61.529752385002,288.727155406076,102.036129697856,0.825,0.824175021234372,122.508156214242,65.65424689215057 +0.0,0.1,46400.0,1.27139563873693,48.2751901598378,288.727155406076,102.036129697856,0.8,0.799200020590906,122.508156214242,61.37686622841374 +0.0,0.1,44950.0,1.49055357865882,39.0529730041172,288.727155406076,102.036129697856,0.775,0.77422501994744,122.508156214242,58.21054866855318 +0.0,0.1,43500.0,1.74741166189252,31.7971341312229,288.727155406076,102.036129697856,0.75,0.749250019303974,122.508156214242,55.56268299565958 +0.0,0.1,42050.0,2.09733168791679,25.274786200372,288.727155406076,102.036129697856,0.725,0.724275018660508,122.508156214242,53.00961000336219 +0.0,0.1,40600.0,2.6200205283376,19.2702125463316,288.727155406076,102.036129697856,0.7,0.699300018017043,122.508156214242,50.488352456817566 +0.0,0.1,0.0,0.0,0.0,288.727155406076,102.036129697856,0.0,0.0,0.0,0.0 +0.0,0.15,58000.0,0.508303822197168,256.522280001327,289.448599663671,102.930036212294,1.0,0.997754256213846,183.762234321362,130.39125540340663 +0.0,0.15,56550.0,0.513768108402086,231.834139600599,289.448599663671,102.930036212294,0.975,0.9728103998085,183.762234321362,119.10898736562488 +0.0,0.15,55100.0,0.53169036295966,205.18808691688,289.448599663671,102.930036212294,0.95,0.947866543403153,183.762234321362,109.09652840783419 +0.0,0.15,53650.0,0.562885984655253,177.074104192777,289.448599663671,102.930036212294,0.925,0.922922686997807,183.762234321362,99.67253149549815 +0.0,0.15,52200.0,0.621415585310371,144.607194862463,289.448599663671,102.930036212294,0.9,0.897978830592461,183.762234321362,89.8611646355483 +0.0,0.15,50750.0,0.724398298070544,110.353702900077,289.448599663671,102.930036212294,0.875,0.873034974187115,183.762234321362,79.94003456659823 +0.0,0.15,49300.0,0.868963419440882,82.5696612854769,289.448599663671,102.930036212294,0.85,0.848091117781769,183.762234321362,71.75001521270342 +0.0,0.15,47850.0,1.04936322033586,62.4692059005246,289.448599663671,102.930036212294,0.825,0.823147261376423,183.762234321362,65.5528870755984 +0.0,0.15,46400.0,1.24760086285893,49.0811249323403,289.448599663671,102.930036212294,0.8,0.798203404971076,183.762234321362,61.2336538156747 +0.0,0.15,44950.0,1.46065400610989,39.7418051078847,289.448599663671,102.930036212294,0.775,0.77325954856573,183.762234321362,58.04902684087028 +0.0,0.15,43500.0,1.71166228196001,32.3575211803761,289.448599663671,102.930036212294,0.75,0.748315692160384,183.762234321362,55.385148542171905 +0.0,0.15,42050.0,2.05587560606064,25.6882448380733,289.448599663671,102.930036212294,0.725,0.723371835755038,183.762234321362,52.81183592510804 +0.0,0.15,40600.0,2.56982715771228,19.5585663923114,289.448599663671,102.930036212294,0.7,0.698427979349692,183.762234321362,50.262135080880526 +0.0,0.15,0.0,0.0,0.0,289.448599663671,102.930036212294,0.0,0.0,0.0,0.0 +0.0,0.2,58000.0,0.501250800659334,261.759459202845,290.458621624305,104.190878817561,1.0,0.996017982548927,245.016312428483,131.20713850558033 +0.0,0.2,56550.0,0.506402418440838,236.701150317205,290.458621624305,104.190878817561,0.975,0.971117532985204,245.016312428483,119.86603496836094 +0.0,0.2,55100.0,0.524081760131972,209.382452906353,290.458621624305,104.190878817561,0.95,0.946217083421481,245.016312428483,109.73352445991122 +0.0,0.2,53650.0,0.554408655520227,180.606119878927,290.458621624305,104.190878817561,0.925,0.921316633857758,245.016312428483,100.12959610080085 +0.0,0.2,52200.0,0.611924089122952,147.248936419038,290.458621624305,104.190878817561,0.9,0.896416184294035,245.016312428483,90.1051712925433 +0.0,0.2,50750.0,0.712867614487687,112.162709165899,290.458621624305,104.190878817561,0.875,0.871515734730311,245.016312428483,79.95716291757064 +0.0,0.2,49300.0,0.853191265962915,83.9669272358269,290.458621624305,104.190878817561,0.85,0.846615285166588,245.016312428483,71.63984894735111 +0.0,0.2,47850.0,1.02520783334495,63.8070427154862,290.458621624305,104.190878817561,0.825,0.821714835602865,245.016312428483,65.41548001449227 +0.0,0.2,46400.0,1.21506130958451,50.2323898197981,290.458621624305,104.190878817561,0.8,0.796814386039142,245.016312428483,61.03543335800349 +0.0,0.2,44950.0,1.41996403959041,40.7236070141082,290.458621624305,104.190878817561,0.775,0.771913936475419,245.016312428483,57.82605752244543 +0.0,0.2,43500.0,1.66295238675979,33.1619847107919,290.458621624305,104.190878817561,0.75,0.747013486911695,245.016312428483,55.14680162450305 +0.0,0.2,42050.0,1.99941687675703,26.2756343548397,290.458621624305,104.190878817561,0.725,0.722113037347972,245.016312428483,52.53594677656331 +0.0,0.2,40600.0,2.50119886215125,19.9707328363875,290.458621624305,104.190878817561,0.7,0.697212587784249,245.016312428483,49.95077424669903 +0.0,0.2,0.0,0.0,0.0,290.458621624305,104.190878817561,0.0,0.0,0.0,0.0 +0.0,0.25,58000.0,0.492562714842441,268.601992942731,291.757221287976,105.828114886451,1.0,0.993798892524202,306.270390535604,132.30332685596176 +0.0,0.25,56550.0,0.497354898953314,243.004445916489,291.757221287976,105.828114886451,0.975,0.968953920211097,306.270390535604,120.85945164400144 +0.0,0.25,55100.0,0.514640513982778,214.839918815304,291.757221287976,105.828114886451,0.95,0.944108947897992,306.270390535604,110.56532624312635 +0.0,0.25,53650.0,0.543944515764917,185.170887141512,291.757221287976,105.828114886451,0.925,0.919263975584887,306.270390535604,100.72268853994983 +0.0,0.25,52200.0,0.600220793043495,150.628263370275,291.757221287976,105.828114886451,0.9,0.894419003271782,306.270390535604,90.41021569487089 +0.0,0.25,50750.0,0.698506218063967,114.489697130401,291.757221287976,105.828114886451,0.875,0.869574030958677,306.270390535604,79.97176534984541 +0.0,0.25,49300.0,0.83342174200584,85.8000169578578,291.757221287976,105.828114886451,0.85,0.844729058645572,306.270390535604,71.50759959714846 +0.0,0.25,47850.0,0.995097049054201,65.5677696774729,291.757221287976,105.828114886451,0.825,0.819884086332467,306.270390535604,65.2462941191188 +0.0,0.25,46400.0,1.17446559612791,51.7593888654142,291.757221287976,105.828114886451,0.8,0.795039114019362,306.270390535604,60.789621499035 +0.0,0.25,44950.0,1.36938137294802,42.0245346692307,291.757221287976,105.828114886451,0.775,0.770194141706257,306.270390535604,57.5476149828528 +0.0,0.25,43500.0,1.60268807424156,34.2200050133504,291.757221287976,105.828114886451,0.75,0.745349169393151,306.270390535604,54.84399393538308 +0.0,0.25,42050.0,1.92925158155243,27.0448145664854,291.757221287976,105.828114886451,0.725,0.720504197080046,306.270390535604,52.17625127518416 +0.0,0.25,40600.0,2.41452165781416,20.5214146028223,291.757221287976,105.828114886451,0.7,0.695659224766941,306.270390535604,49.549400007498214 +0.0,0.25,0.0,0.0,0.0,291.757221287976,105.828114886451,0.0,0.0,0.0,0.0 +0.0,0.3,58000.0,0.482367372847481,277.126239778584,293.344398654686,107.854024062161,1.0,0.991106708010945,367.524468642725,133.67665622909666 +0.0,0.3,56550.0,0.486870031914151,250.789144178591,293.344398654686,107.854024062161,0.975,0.966329040310672,367.524468642725,122.10171862995321 +0.0,0.3,55100.0,0.503598043800678,221.611062031802,293.344398654686,107.854024062161,0.95,0.941551372610398,367.524468642725,111.60289732380618 +0.0,0.3,53650.0,0.531818601227221,190.750348595921,293.344398654686,107.854024062161,0.925,0.916773704910124,367.524468642725,101.44458357388751 +0.0,0.3,52200.0,0.586686128176419,154.692509344946,293.344398654686,107.854024062161,0.9,0.891996037209851,367.524468642725,90.75594936548087 +0.0,0.3,50750.0,0.681053189804184,117.520945887004,293.344398654686,107.854024062161,0.875,0.867218369509577,367.524468642725,80.03801506514897 +0.0,0.3,49300.0,0.810028838788352,88.0704136228099,293.344398654686,107.854024062161,0.85,0.842440701809304,367.524468642725,71.33957487849456 +0.0,0.3,47850.0,0.959664690959701,67.7841412516794,293.344398654686,107.854024062161,0.825,0.81766303410903,367.524468642725,65.05004696626163 +0.0,0.3,46400.0,1.12676432858678,53.6995558374525,293.344398654686,107.854024062161,0.8,0.792885366408756,367.524468642725,60.50674397859547 +0.0,0.3,44950.0,1.31026983912815,43.6700802700707,293.344398654686,107.854024062161,0.775,0.768107698708483,367.524468642725,57.21958905017893 +0.0,0.3,43500.0,1.53250532677088,35.5476521639635,293.344398654686,107.854024062161,0.75,0.743330031008209,367.524468642725,54.47696629547246 +0.0,0.3,42050.0,1.84715125774841,28.0133860735945,293.344398654686,107.854024062161,0.725,0.718552363307935,367.524468642725,51.74496131963187 +0.0,0.3,40600.0,2.31213798797291,21.2209657867659,293.344398654686,107.854024062161,0.7,0.693774695607662,367.524468642725,49.06580113705487 +0.0,0.3,0.0,0.0,0.0,293.344398654686,107.854024062161,0.0,0.0,0.0,0.0 +0.0,0.35,58000.0,0.470982676252837,287.448191862454,295.220153724433,110.283798947079,1.0,0.987953068515966,428.778546749846,135.38311868741755 +0.0,0.35,56550.0,0.475218114456363,260.108270129808,295.220153724433,110.283798947079,0.975,0.963254241803067,428.778546749846,123.60816168559369 +0.0,0.35,55100.0,0.491174946865564,229.795617605291,295.220153724433,110.283798947079,0.95,0.938555415090168,428.778546749846,112.86985026721827 +0.0,0.35,53650.0,0.518318948567858,197.334217399414,295.220153724433,110.283798947079,0.925,0.913856588377269,428.778546749846,102.28206407892537 +0.0,0.35,52200.0,0.571682608308068,159.409924689662,295.220153724433,110.283798947079,0.9,0.889157761664369,428.778546749846,91.13188153677866 +0.0,0.35,50750.0,0.66132568386357,121.135080521774,295.220153724433,110.283798947079,0.875,0.86445893495147,428.778546749846,80.10973996593081 +0.0,0.35,49300.0,0.783144154629023,90.8624063235755,295.220153724433,110.283798947079,0.85,0.839760108238571,428.778546749846,71.15836238783533 +0.0,0.35,47850.0,0.91948639236671,70.5006201141703,295.220153724433,110.283798947079,0.825,0.815061281525672,428.778546749846,64.82436084839436 +0.0,0.35,46400.0,1.0730676040304,56.0937180021038,295.220153724433,110.283798947079,0.8,0.790362454812773,428.778546749846,60.19235157767444 +0.0,0.35,44950.0,1.24430053465644,45.6846095464051,295.220153724433,110.283798947079,0.775,0.765663628099874,428.778546749846,56.84538408416257 +0.0,0.35,43500.0,1.45417587540355,37.1632115898238,295.220153724433,110.283798947079,0.75,0.740964801386974,428.778546749846,54.041845746439385 +0.0,0.35,42050.0,1.75444595442007,29.1975393285886,295.220153724433,110.283798947079,0.725,0.716265974674075,428.778546749846,51.22550475406315 +0.0,0.35,40600.0,2.19546192091381,22.0904790876996,295.220153724433,110.283798947079,0.7,0.691567147961176,428.778546749846,48.49880565178731 +0.0,0.35,0.0,0.0,0.0,295.220153724433,110.283798947079,0.0,0.0,0.0,0.0 +1000.0,0.0,58000.0,0.515510044506519,231.347045275657,281.65,89.8745653777784,1.0,1.01147333392362,0.0,119.26172560650562 +1000.0,0.0,56550.0,0.521319051346396,208.529434436095,281.65,89.8745653777784,0.975,0.986186500575529,0.0,108.71036693802552 +1000.0,0.0,55100.0,0.533408708360696,186.672798453626,281.65,89.8745653777784,0.95,0.960899667227439,0.0,99.57289630922517 +1000.0,0.0,53650.0,0.561805068138577,162.286894360006,281.65,89.8745653777784,0.925,0.935612833879348,0.0,91.17359974392122 +1000.0,0.0,52200.0,0.608779472420338,136.377620387831,281.65,89.8745653777784,0.9,0.910326000531258,0.0,83.0238957896449 +1000.0,0.0,50750.0,0.698825801404107,106.340081289564,281.65,89.8745653777784,0.875,0.885039167183167,0.0,74.31319252855745 +1000.0,0.0,49300.0,0.837141835069454,79.542500537258,281.65,89.8745653777784,0.85,0.859752333835077,0.0,66.5883548657732 +1000.0,0.0,47850.0,1.02541544810262,59.0387574346416,281.65,89.8745653777784,0.825,0.834465500486986,0.0,60.53925391026491 +1000.0,0.0,46400.0,1.24073035229992,45.3425702128889,281.65,89.8745653777784,0.8,0.809178667138896,0.0,56.25790311442151 +1000.0,0.0,44950.0,1.47134751687178,36.1922572739731,281.65,89.8745653777784,0.775,0.783891833790805,0.0,53.25138787004494 +1000.0,0.0,43500.0,1.73333034540858,29.3477867111523,281.65,89.8745653777784,0.75,0.758605000442715,0.0,50.869409277018946 +1000.0,0.0,42050.0,2.07333744397542,23.4978035602648,281.65,89.8745653777784,0.725,0.733318167094624,0.0,48.71887597267594 +1000.0,0.0,40600.0,2.58613994598036,18.0061465244471,281.65,89.8745653777784,0.7,0.708031333746534,0.0,46.56641480004807 +0.0,0.0,0.0,0.0,0.0,281.65,89.8745653777784,0.0,0.0,0.0,0.0 +1000.0,0.05,58000.0,0.514407544001346,232.057071431022,281.791043194302,90.0319709371256,1.0,1.01122016887588,60.5612300201938,119.37190818297695 +1000.0,0.05,56550.0,0.520197895272965,209.155609403893,281.791043194302,90.0319709371256,0.975,0.985939664653983,60.5612300201938,108.80230779643951 +1000.0,0.05,55100.0,0.532275217566708,187.223180163877,281.791043194302,90.0319709371256,0.95,0.960659160432086,60.5612300201938,99.65425895525861 +1000.0,0.05,53650.0,0.56055009449265,162.762944486508,281.791043194302,90.0319709371256,0.925,0.935378656210189,60.5612300201938,91.236783911814 +1000.0,0.05,52200.0,0.607406502827473,136.748078761447,281.791043194302,90.0319709371256,0.9,0.910098151988292,60.5612300201938,83.06167228886636 +1000.0,0.05,50750.0,0.697187921761441,106.606249474818,281.791043194302,90.0319709371256,0.875,0.884817647766395,60.5612300201938,74.32458951813007 +1000.0,0.05,49300.0,0.835046324508423,79.7276235815022,281.791043194302,90.0319709371256,0.85,0.859537143544498,60.5612300201938,66.57625903352448 +1000.0,0.05,47850.0,1.02226108118176,59.2005754221843,281.791043194302,90.0319709371256,0.825,0.834256639322601,60.5612300201938,60.518444237664454 +1000.0,0.05,46400.0,1.2361741222274,45.487062658334,281.791043194302,90.0319709371256,0.8,0.808976135100704,60.5612300201938,56.229929754368776 +1000.0,0.05,44950.0,1.46543348145882,36.3170660139083,281.791043194302,90.0319709371256,0.775,0.783695630878807,60.5612300201938,53.22024448513143 +1000.0,0.05,43500.0,1.7260791323154,29.4521460135816,281.791043194302,90.0319709371256,0.75,0.75841512665691,60.5612300201938,50.8367346359494 +1000.0,0.05,42050.0,2.06476425078307,23.577492698345,281.791043194302,90.0319709371256,0.725,0.733134622435013,60.5612300201938,48.681964046641625 +1000.0,0.05,40600.0,2.57601707924468,18.0605120273877,281.791043194302,90.0319709371256,0.7,0.707854118213116,60.5612300201938,46.524187442454675 +1000.0,0.05,0.0,0.0,0.0,281.791043194302,90.0319709371256,0.0,0.0,0.0,0.0 +1000.0,0.1,58000.0,0.511323931509567,234.051973284574,282.214172777207,90.5053684407736,1.0,1.01046181279174,121.122460040388,119.67637515744052 +1000.0,0.1,56550.0,0.516860901055493,211.043872820739,282.214172777207,90.5053684407736,0.975,0.985200267471946,121.122460040388,109.08032626836803 +1000.0,0.1,55100.0,0.528913100039104,188.876591961705,282.214172777207,90.5053684407736,0.95,0.959938722152153,121.122460040388,99.8993037792863 +1000.0,0.1,53650.0,0.556815473768111,164.196894302291,282.214172777207,90.5053684407736,0.925,0.934677176832359,121.122460040388,91.42737149218262 +1000.0,0.1,52200.0,0.603333857444066,137.857859017078,282.214172777207,90.5053684407736,0.9,0.909415631512566,121.122460040388,83.17431385975388 +1000.0,0.1,50750.0,0.692315772188055,107.405528367254,282.214172777207,90.5053684407736,0.875,0.884154086192772,121.122460040388,74.3585413088415 +1000.0,0.1,49300.0,0.828802161037887,80.2847066192652,282.214172777207,90.5053684407736,0.85,0.858892540872979,121.122460040388,66.54013834433974 +1000.0,0.1,47850.0,1.01285752406058,59.6891383315025,282.214172777207,90.5053684407736,0.825,0.833630995553185,121.122460040388,60.45659286375508 +1000.0,0.1,46400.0,1.22260490227503,45.9239216945485,282.214172777207,90.5053684407736,0.8,0.808369450233392,121.122460040388,56.1468117954496 +1000.0,0.1,44950.0,1.44784591856524,36.6940706105627,282.214172777207,90.5053684407736,0.775,0.783107904913598,121.122460040388,53.12736036904792 +1000.0,0.1,43500.0,1.70447817617648,29.7676876469067,282.214172777207,90.5053684407736,0.75,0.757846359593805,121.122460040388,50.73837394939067 +1000.0,0.1,42050.0,2.03934058277994,23.8173918741994,282.214172777207,90.5053684407736,0.725,0.732584814274011,121.122460040388,48.57177382502802 +1000.0,0.1,40600.0,2.54582687956037,18.2250708190903,282.214172777207,90.5053684407736,0.7,0.707323268954218,121.122460040388,46.39787517313141 +1000.0,0.1,0.0,0.0,0.0,282.214172777207,90.5053684407736,0.0,0.0,0.0,0.0 +1000.0,0.15,58000.0,0.506314395500302,237.404529912313,282.919388748717,91.2983092035152,1.0,1.00920166864562,181.683690060581,120.20133105158612 +1000.0,0.15,56550.0,0.511387888409563,214.222716976481,282.919388748717,91.2983092035152,0.975,0.983971626929483,181.683690060581,109.55090288396205 +1000.0,0.15,55100.0,0.52341748065481,191.646427610897,282.919388748717,91.2983092035152,0.95,0.958741585213343,181.683690060581,100.31109031659012 +1000.0,0.15,53650.0,0.550690020565192,166.605593921518,282.919388748717,91.2983092035152,0.925,0.933511543497202,181.683690060581,91.74803794291677 +1000.0,0.15,52200.0,0.596790691764497,139.667866391822,282.919388748717,91.2983092035152,0.9,0.908281501781062,181.683690060581,83.35248260124679 +1000.0,0.15,50750.0,0.684314755335882,108.744814355104,282.919388748717,91.2983092035152,0.875,0.883051460064921,181.683690060581,74.41568102945891 +1000.0,0.15,49300.0,0.818584457801641,81.2116685319316,282.919388748717,91.2983092035152,0.85,0.85782141834878,181.683690060581,66.47860965237781 +1000.0,0.15,47850.0,0.997361889035037,60.5150997924293,282.919388748717,91.2983092035152,0.825,0.83259137663264,181.683690060581,60.355454244121056 +1000.0,0.15,46400.0,1.20030024896332,46.6640080678968,282.919388748717,91.2983092035152,0.8,0.807361334916499,181.683690060581,56.01082050152291 +1000.0,0.15,44950.0,1.41899676814152,37.3320805814069,282.919388748717,91.2983092035152,0.775,0.782131293200358,181.683690060581,52.97410169301519 +1000.0,0.15,43500.0,1.66918699537015,30.2996289602214,282.919388748717,91.2983092035152,0.75,0.756901251484218,181.683690060581,50.57574662494234 +1000.0,0.15,42050.0,1.99770934875897,24.2225459222734,282.919388748717,91.2983092035152,0.725,0.731671209768077,181.683690060581,48.389606439669045 +1000.0,0.15,40600.0,2.49619212322723,18.5037167448737,282.919388748717,91.2983092035152,0.7,0.706441168051937,181.683690060581,46.188831988981526 +1000.0,0.15,0.0,0.0,0.0,289.448599663671,102.930036212294,0.0,0.0,0.0,0.0 +1000.0,0.2,58000.0,0.499500545527715,242.133031404394,283.90669110883,92.4167415738559,1.0,1.00744536106482,242.244920080775,120.94558127677415 +1000.0,0.2,56550.0,0.503928165349098,218.725359848472,283.90669110883,92.4167415738559,0.975,0.982259227038199,242.244920080775,110.22186930376176 +1000.0,0.2,55100.0,0.515952727887589,195.550713428736,283.90669110883,92.4167415738559,0.95,0.957073093011579,242.244920080775,100.89492403392052 +1000.0,0.2,53650.0,0.542317761076815,170.019365303365,283.90669110883,92.4167415738559,0.925,0.931886958984958,242.244920080775,92.20452153102202 +1000.0,0.2,52200.0,0.587897595951998,142.199704887738,283.90669110883,92.4167415738559,0.9,0.906700824958338,242.244920080775,83.59886464858474 +1000.0,0.2,50750.0,0.673398464263004,110.622962113858,283.90669110883,92.4167415738559,0.875,0.881514690931717,242.244920080775,74.49333279969645 +1000.0,0.2,49300.0,0.804574283732402,82.5201578438976,283.90669110883,92.4167415738559,0.85,0.856328556905097,242.244920080775,66.39359689073866 +1000.0,0.2,47850.0,0.976056231812973,61.6956889529813,283.90669110883,92.4167415738559,0.825,0.831142422878476,242.244920080775,60.21846167855219 +1000.0,0.2,46400.0,1.16979524635858,47.7229964225794,283.90669110883,92.4167415738559,0.8,0.805956288851856,242.244920080775,55.82613435712089 +1000.0,0.2,44950.0,1.37966973395626,38.2438230126976,283.90669110883,92.4167415738559,0.775,0.780770154825235,242.244920080775,52.76384512139879 +1000.0,0.2,43500.0,1.621139175837,31.0584541615992,283.90669110883,92.4167415738559,0.75,0.755584020798615,242.244920080775,50.35007678230617 +1000.0,0.2,42050.0,1.94088839464961,24.7984406532303,283.90669110883,92.4167415738559,0.725,0.730397886771994,242.244920080775,48.13100566926179 +1000.0,0.2,40600.0,2.42776174404928,18.9045997432519,283.90669110883,92.4167415738559,0.7,0.705211752745374,242.244920080775,45.8958640432308 +1000.0,0.2,0.0,0.0,0.0,283.90669110883,92.4167415738559,0.0,0.0,0.0,0.0 +1000.0,0.25,58000.0,0.491106870834565,248.295084764935,285.176079857547,93.8690552767738,1.0,1.00520066703131,302.806150100969,121.93942212251031 +1000.0,0.25,56550.0,0.494674130615802,224.619478913764,285.176079857547,93.8690552767738,0.975,0.980070650355526,302.806150100969,111.11344545104068 +1000.0,0.25,55100.0,0.506710974296498,200.636726936331,285.176079857547,93.8690552767738,0.95,0.954940633679743,302.806150100969,101.66483138556872 +1000.0,0.25,53650.0,0.531891669068342,174.48256837113,285.176079857547,93.8690552767738,0.925,0.92981061700396,302.806150100969,92.80582451425141 +1000.0,0.25,52200.0,0.576834995127259,145.458943806754,285.176079857547,93.8690552767738,0.9,0.904680600328178,302.806150100969,83.90580914198519 +1000.0,0.25,50750.0,0.659833245731738,113.042614094839,285.176079857547,93.8690552767738,0.875,0.879550583652395,302.806150100969,74.58927496419793 +1000.0,0.25,49300.0,0.787113280285323,84.2161426694686,285.176079857547,93.8690552767738,0.85,0.854420566976612,302.806150100969,66.28764430954219 +1000.0,0.25,47850.0,0.949270548719875,63.2601066250733,285.176079857547,93.8690552767738,0.825,0.829290550300829,302.806150100969,60.05095612806113 +1000.0,0.25,46400.0,1.13176060787858,49.1256977066405,285.176079857547,93.8690552767738,0.8,0.804160533625047,302.806150100969,55.598529498926816 +1000.0,0.25,44950.0,1.33080591702903,39.4500197423346,285.176079857547,93.8690552767738,0.775,0.779030516949264,302.806150100969,52.500319700010934 +1000.0,0.25,43500.0,1.56164397544693,32.0589300930277,285.176079857547,93.8690552767738,0.75,0.753900500273481,302.806150100969,50.06463503905099 +1000.0,0.25,42050.0,1.87068932127897,25.5556436643225,285.176079857547,93.8690552767738,0.725,0.728770483597698,302.806150100969,47.80666970125866 +1000.0,0.25,40600.0,2.34272339485242,19.434349895432,285.176079857547,93.8690552767738,0.7,0.703640466921916,302.806150100969,45.529306163776226 +1000.0,0.25,0.0,0.0,0.0,285.176079857547,93.8690552767738,0.0,0.0,0.0,0.0 +1000.0,0.3,58000.0,0.481334847455855,255.920202280522,286.727554994867,95.6661437070395,1.0,1.00247742120916,363.367380121163,123.18331152556661 +1000.0,0.3,56550.0,0.483921798146751,231.894451240306,286.727554994867,95.6661437070395,0.975,0.977415485678929,363.367380121163,112.21877982446296 +1000.0,0.3,55100.0,0.495933328551175,206.933825773019,286.727554994867,95.6661437070395,0.95,0.9523535501487,363.367380121163,102.62538100544224 +1000.0,0.3,53650.0,0.519736573799463,179.984095165987,286.727554994867,95.6661437070395,0.925,0.927291614618471,363.367380121163,93.54431695996658 +1000.0,0.3,52200.0,0.563971973204375,149.462959868794,286.727554994867,95.6661437070395,0.9,0.902229679088242,363.367380121163,84.29292039817007 +1000.0,0.3,50750.0,0.643942242463018,115.999665097739,286.727554994867,95.6661437070395,0.875,0.877167743558013,363.367380121163,74.69708446799713 +1000.0,0.3,49300.0,0.766418607040161,86.3322915762664,286.727554994867,95.6661437070395,0.85,0.852105808027784,363.367380121163,66.16667465246712 +1000.0,0.3,47850.0,0.917325200115547,65.2429981447152,286.727554994867,95.6661437070395,0.825,0.827043872497555,363.367380121163,59.84904632923913 +1000.0,0.3,46400.0,1.08686716694676,50.9079859557327,286.727554994867,95.6661437070395,0.8,0.801981936967326,363.367380121163,55.33021847067264 +1000.0,0.3,44950.0,1.27353794841834,40.9857875552953,286.727554994867,95.6661437070395,0.775,0.776920001437097,363.367380121163,52.19695579748071 +1000.0,0.3,43500.0,1.49236777331988,33.3168463018979,286.727554994867,95.6661437070395,0.75,0.751858065906868,363.367380121163,49.72098772960404 +1000.0,0.3,42050.0,1.78881980291953,26.5065014540787,286.727554994867,95.6661437070395,0.725,0.726796130376639,363.367380121163,47.41535470717129 +1000.0,0.3,40600.0,2.24145710517143,20.1131429503249,286.727554994867,95.6661437070395,0.7,0.70173419484641,363.367380121163,45.082747173334404 +1000.0,0.3,0.0,0.0,0.0,286.727554994867,95.6661437070395,0.0,0.0,0.0,0.0 +1000.0,0.35,58000.0,0.47039404555375,265.043478251638,288.561116520791,97.8214843854198,1.0,0.999287398130672,423.928610141357,124.67487398242535 +1000.0,0.35,56550.0,0.472045317312734,240.596937246103,288.561116520791,97.8214843854198,0.975,0.974305213177405,423.928610141357,113.57265758680865 +1000.0,0.35,55100.0,0.48389604314158,214.471789263338,288.561116520791,97.8214843854198,0.95,0.949323028224138,423.928610141357,103.78205019002407 +1000.0,0.35,53650.0,0.506310938509629,186.502342985338,288.561116520791,97.8214843854198,0.925,0.924340843270871,423.928610141357,94.42817631115122 +1000.0,0.35,52200.0,0.549551042070556,154.193828903687,288.561116520791,97.8214843854198,0.9,0.899358658317605,423.928610141357,84.73737935487021 +1000.0,0.35,50750.0,0.625907160843942,119.560597867778,288.561116520791,97.8214843854198,0.875,0.874376473364338,423.928610141357,74.8338343602252 +1000.0,0.35,49300.0,0.742637052860997,88.9166687625891,288.561116520791,97.8214843854198,0.85,0.849394288411071,423.928610141357,66.03281284006664 +1000.0,0.35,47850.0,0.88132580473439,67.6577944554284,288.561116520791,97.8214843854198,0.825,0.824412103457804,423.928610141357,59.62856014498439 +1000.0,0.35,46400.0,1.03637969937351,53.1367453506142,288.561116520791,97.8214843854198,0.8,0.799429918504537,423.928610141357,55.069844172156294 +1000.0,0.35,44950.0,1.20954895436051,42.8644097074849,288.561116520791,97.8214843854198,0.775,0.774447733551271,423.928610141357,51.84660194096886 +1000.0,0.35,43500.0,1.41534127897073,34.842722610254,288.561116520791,97.8214843854198,0.75,0.749465548598004,423.928610141357,49.31434358201927 +1000.0,0.35,42050.0,1.69712370225193,27.6602108108716,288.561116520791,97.8214843854198,0.725,0.724483363644737,423.928610141357,46.94279937641527 +1000.0,0.35,40600.0,2.17503761097496,20.0585876591485,288.561116520791,97.8214843854198,0.7,0.69950117869147,423.928610141357,43.62818258168617 +1000.0,0.35,0.0,0.0,0.0,288.561116520791,97.8214843854198,0.0,0.0,0.0,0.0 +2000.0,0.0,58000.0,0.515040664520812,213.569545962531,275.15,79.495201431303,1.0,1.02335085536676,0.0,109.99700087395007 +2000.0,0.0,56550.0,0.519299186792368,192.755816584944,275.15,79.495201431303,0.975,0.997767083982586,0.0,100.09793880206026 +2000.0,0.0,55100.0,0.528089114170692,173.16041580167,275.15,79.495201431303,0.95,0.972183312598417,0.0,91.44413059013262 +2000.0,0.0,53650.0,0.550795550902113,152.166960620804,275.15,79.495201431303,0.925,0.946599541214248,0.0,83.81288490423586 +2000.0,0.0,52200.0,0.590253307886354,129.83818614625,275.15,79.495201431303,0.9,0.92101576983008,0.0,76.63741886278824 +2000.0,0.0,50750.0,0.663847887312897,104.040691406009,275.15,79.495201431303,0.875,0.895431998445911,0.0,69.06719318445215 +2000.0,0.0,49300.0,0.788899101634556,78.3153168982003,275.15,79.495201431303,0.85,0.869848227061742,0.0,61.78288314521578 +2000.0,0.0,47850.0,0.967447846640383,57.8097254335603,275.15,79.495201431303,0.825,0.844264455677573,0.0,55.927894385569694 +2000.0,0.0,46400.0,1.18925988996186,43.3988491834337,275.15,79.495201431303,0.8,0.818680684293404,0.0,51.61251060436172 +2000.0,0.0,44950.0,1.42866392429822,34.1105861782349,275.15,79.495201431303,0.775,0.793096912909235,0.0,48.732563909509686 +2000.0,0.0,43500.0,1.69672297450453,27.4562797051368,275.15,79.495201431303,0.75,0.767513141525066,0.0,46.585700570128076 +2000.0,0.0,42050.0,2.02976391081085,22.0427161641825,275.15,79.495201431303,0.725,0.741929370140897,0.0,44.74150976630461 +2000.0,0.0,40600.0,2.52252750412449,17.0250742966338,275.15,79.495201431303,0.7,0.716345598756728,0.0,42.94621817302166 +2000.0,0.0,0.0,0.0,0.0,275.15,79.495201431303,0.0,0.0,0.0,0.0 +2000.0,0.05,58000.0,0.513998514586902,214.194454653585,275.287797113977,79.6344377189261,1.0,1.02309470081355,59.8602712653275,110.09563152469424 +2000.0,0.05,56550.0,0.518292761439053,193.248370151508,275.287797113977,79.6344377189261,0.975,0.997517333293213,59.8602712653275,100.15923140942135 +2000.0,0.05,55100.0,0.526954837499877,173.679538868261,275.287797113977,79.6344377189261,0.95,0.971939965772874,59.8602712653275,91.52127318137805 +2000.0,0.05,53650.0,0.549638241152388,152.602234420244,275.287797113977,79.6344377189261,0.925,0.946362598252535,59.8602712653275,83.87602372266733 +2000.0,0.05,52200.0,0.588924479021849,130.206082713338,275.287797113977,79.6344377189261,0.9,0.920785230732197,59.8602712653275,76.68154942742835 +2000.0,0.05,50750.0,0.662263113328942,104.324346491018,275.287797113977,79.6344377189261,0.875,0.895207863211858,59.8602712653275,69.09016650314888 +2000.0,0.05,49300.0,0.787023024813282,78.4974207861195,275.287797113977,79.6344377189261,0.85,0.869630495691519,59.8602712653275,61.77927754713277 +2000.0,0.05,47850.0,0.96477046936105,57.9481248111078,275.287797113977,79.6344377189261,0.825,0.84405312817118,59.8602712653275,55.90663957260519 +2000.0,0.05,46400.0,1.18493401412138,43.5394583586965,275.287797113977,79.6344377189261,0.8,0.818475760650841,59.8602712653275,51.591385165640915 +2000.0,0.05,44950.0,1.42299961041599,34.2275854226577,275.287797113977,79.6344377189261,0.775,0.792898393130502,59.8602712653275,48.70584072192192 +2000.0,0.05,43500.0,1.68953382430745,27.5545073182161,275.287797113977,79.6344377189261,0.75,0.767321025610164,59.8602712653275,46.55427212625327 +2000.0,0.05,42050.0,2.02157328496948,22.1156665748395,275.287797113977,79.6344377189261,0.725,0.741743658089825,59.8602712653275,44.70844072698802 +2000.0,0.05,40600.0,2.51233778089155,17.0812536237758,275.287797113977,79.6344377189261,0.7,0.716166290569486,59.8602712653275,42.913878824002644 +2000.0,0.05,0.0,0.0,0.0,275.287797113977,79.6344377189261,0.0,0.0,59.8602712653275,0.0 +2000.0,0.1,58000.0,0.510919083743302,216.065589871836,275.70118845591,80.0531911732792,1.0,1.0223273897384,119.720542530655,110.39203320577452 +2000.0,0.1,56550.0,0.515125202745729,194.911234844513,275.70118845591,80.0531911732792,0.975,0.996769204994944,119.720542530655,100.40368936670016 +2000.0,0.1,55100.0,0.523544919442829,175.258927837278,275.70118845591,80.0531911732792,0.95,0.971211020251484,119.720542530655,91.75592125620429 +2000.0,0.1,53650.0,0.546104394811035,153.922796754545,275.70118845591,80.0531911732792,0.925,0.945652835508024,119.720542530655,84.05791576926275 +2000.0,0.1,52200.0,0.584972407301244,131.296458225794,275.70118845591,80.0531911732792,0.9,0.920094650764564,119.720542530655,76.80480523846994 +2000.0,0.1,50750.0,0.657612822883715,105.15383292654,275.70118845591,80.0531911732792,0.875,0.894536466021103,119.720542530655,69.15050890786452 +2000.0,0.1,49300.0,0.78139803562091,79.0433486403363,275.70118845591,80.0531911732792,0.85,0.868978281277643,119.720542530655,61.76431735645752 +2000.0,0.1,47850.0,0.956657672643004,58.3836603153741,275.70118845591,80.0531911732792,0.825,0.843420096534183,119.720542530655,55.853176597685504 +2000.0,0.1,46400.0,1.17240468553146,43.9430413111819,275.70118845591,80.0531911732792,0.8,0.817861911790723,119.720542530655,51.51902752973217 +2000.0,0.1,44950.0,1.4060986663735,34.579567492174,275.70118845591,80.0531911732792,0.775,0.792303727047263,119.720542530655,48.6222837345183 +2000.0,0.1,43500.0,1.66820357321683,27.852059961693,275.70118845591,80.0531911732792,0.75,0.766745542303803,119.720542530655,46.46290594954567 +2000.0,0.1,42050.0,1.99603555324785,22.3509269040472,275.70118845591,80.0531911732792,0.725,0.741187357560343,119.720542530655,44.613244748522106 +2000.0,0.1,40600.0,2.4821583380156,17.2427048424978,275.70118845591,80.0531911732792,0.7,0.715629172816883,119.720542530655,42.79912359474788 +2000.0,0.1,0.0,0.0,0.0,275.70118845591,80.0531911732792,0.0,0.0,119.720542530655,0.0 +2000.0,0.15,58000.0,0.505940469287619,219.170507032382,276.390174025797,80.7546033876493,1.0,1.02105236552231,179.580813795983,110.88722918196876 +2000.0,0.15,56550.0,0.509876075856986,197.741197803105,276.390174025797,80.7546033876493,0.975,0.995526056384251,179.580813795983,100.82350597110724 +2000.0,0.15,55100.0,0.517965667072572,177.90754201704,276.390174025797,80.7546033876493,0.95,0.969999747246193,179.580813795983,92.14999867809775 +2000.0,0.15,53650.0,0.540326499367254,156.13910437807,276.390174025797,80.7546033876493,0.925,0.944473438108135,179.580813795983,84.36609568294084 +2000.0,0.15,52200.0,0.578528560029528,133.112833708327,276.390174025797,80.7546033876493,0.9,0.918947128970078,179.580813795983,77.00957600672844 +2000.0,0.15,50750.0,0.650061689936006,106.523511684329,276.390174025797,80.7546033876493,0.875,0.89342081983202,179.580813795983,69.24685402343279 +2000.0,0.15,49300.0,0.772130195036696,79.9603355666991,276.390174025797,80.7546033876493,0.85,0.867894510693962,179.580813795983,61.73978949631503 +2000.0,0.15,47850.0,0.943383685282142,59.1134126225871,276.390174025797,80.7546033876493,0.825,0.842368201555904,179.580813795983,55.76662904950011 +2000.0,0.15,46400.0,1.15182262373302,44.6256045231463,276.390174025797,80.7546033876493,0.8,0.816841892417847,179.580813795983,51.4007808875225 +2000.0,0.15,44950.0,1.37839265567595,35.1750628777158,276.390174025797,80.7546033876493,0.775,0.791315583279789,179.580813795983,48.48504833358321 +2000.0,0.15,43500.0,1.63334259022189,28.3538801484714,276.390174025797,80.7546033876493,0.75,0.765789274141731,179.580813795983,46.31160004454531 +2000.0,0.15,42050.0,1.95393164948942,22.7457146696403,276.390174025797,80.7546033876493,0.725,0.740262965003674,179.580813795983,44.44357178326597 +2000.0,0.15,40600.0,2.43216017712677,17.5175992884029,276.390174025797,80.7546033876493,0.7,0.714736655865616,179.580813795983,42.60560738811777 +2000.0,0.15,0.0,0.0,0.0,276.390174025797,80.7546033876493,0.0,0.0,179.580813795983,0.0 +2000.0,0.2,58000.0,0.499232130684788,223.499410948191,277.35475382364,81.7439364407149,1.0,1.01927531957574,239.44108506131,111.57808713446043 +2000.0,0.2,56550.0,0.502718503653552,201.74576590171,277.35475382364,81.7439364407149,0.975,0.993793436586342,239.44108506131,101.42132955254743 +2000.0,0.2,55100.0,0.510439467551189,181.625973772937,277.35475382364,81.7439364407149,0.95,0.968311553596949,239.44108506131,92.70906534612416 +2000.0,0.2,53650.0,0.532446869721074,159.26960906653,277.35475382364,81.7439364407149,0.925,0.942829670607555,239.44108506131,84.80260478917309 +2000.0,0.2,52200.0,0.569771402012033,135.661668382115,277.35475382364,81.7439364407149,0.9,0.917347787618162,239.44108506131,77.29613899336915 +2000.0,0.2,50750.0,0.639810490337827,108.43542661099,277.35475382364,81.7439364407149,0.875,0.891865904628768,239.44108506131,69.37812346996897 +2000.0,0.2,49300.0,0.759387811981252,81.2584775475928,277.35475382364,81.7439364407149,0.85,0.866384021639375,239.44108506131,61.70669746979419 +2000.0,0.2,47850.0,0.925360676952596,60.138363527241,277.35475382364,81.7439364407149,0.825,0.840902138649982,239.44108506131,55.649676784389044 +2000.0,0.2,46400.0,1.12346899321624,45.6108797381169,277.35475382364,81.7439364407149,0.8,0.815420255660588,239.44108506131,51.2424091390892 +2000.0,0.2,44950.0,1.34057128208328,36.0265684010973,277.35475382364,81.7439364407149,0.775,0.789938372671195,239.44108506131,48.296182990519995 +2000.0,0.2,43500.0,1.58591300908105,29.0694892137872,277.35475382364,81.7439364407149,0.75,0.764456489681801,239.44108506131,46.101681111486386 +2000.0,0.2,42050.0,1.89686191592384,23.3095712875742,277.35475382364,81.7439364407149,0.725,0.738974606692408,239.44108506131,44.21503805191133 +2000.0,0.2,40600.0,2.3640887401846,17.9122257023548,277.35475382364,81.7439364407149,0.7,0.713492723703015,239.44108506131,42.34609109458217 +2000.0,0.2,0.0,0.0,0.0,277.35475382364,81.7439364407149,0.0,0.0,239.44108506131,0.0 +2000.0,0.25,58000.0,0.490943210954541,229.083884602049,278.594927849437,83.0286121259999,1.0,1.01700412121317,299.301356326638,112.46717788446948 +2000.0,0.25,56550.0,0.493825203742373,206.98265691698,278.594927849437,83.0286121259999,0.975,0.991579018182845,299.301356326638,102.21325272316534 +2000.0,0.25,55100.0,0.501186802222428,186.431460591808,278.594927849437,83.0286121259999,0.95,0.966153915152516,299.301356326638,93.43698756766484 +2000.0,0.25,53650.0,0.52264881073961,163.35157559787,278.594927849437,83.0286121259999,0.925,0.940728812122187,299.301356326638,85.37550671866826 +2000.0,0.25,52200.0,0.55891656606103,138.962133832492,278.594927849437,83.0286121259999,0.9,0.915303709091857,299.301356326638,77.6682386541697 +2000.0,0.25,50750.0,0.627151765084877,110.880795710008,278.594927849437,83.0286121259999,0.875,0.889878606061528,299.301356326638,69.53908674354717 +2000.0,0.25,49300.0,0.743417723081925,82.9462259129654,278.594927849437,83.0286121259999,0.85,0.864453503031198,299.301356326638,61.663694406455704 +2000.0,0.25,47850.0,0.902422639815665,61.497550318509,278.594927849437,83.0286121259999,0.825,0.839028400000869,299.301356326638,55.496781700625576 +2000.0,0.25,46400.0,1.08805487679301,46.9318427594,278.594927849437,83.0286121259999,0.8,0.81360329697054,299.301356326638,51.064420391247886 +2000.0,0.25,44950.0,1.29379201367826,37.1450034063478,278.594927849437,83.0286121259999,0.775,0.78817819394021,299.301356326638,48.05790875518454 +2000.0,0.25,43500.0,1.52724022403009,30.0105866069096,278.594927849437,83.0286121259999,0.75,0.762753090909881,299.301356326638,45.83337501281104 +2000.0,0.25,42050.0,1.82625085802079,24.0544040573426,278.594927849437,83.0286121259999,0.725,0.737327987879551,299.301356326638,43.92937604890069 +2000.0,0.25,40600.0,2.27925095605852,18.434278259216,278.594927849437,83.0286121259999,0.7,0.711902884849222,299.301356326638,42.01634634656686 +2000.0,0.25,0.0,0.0,0.0,278.594927849437,83.0286121259999,0.0,0.0,299.301356326638,0.0 +2000.0,0.3,58000.0,0.481312194233186,235.949797564156,280.110696103189,84.618267061966,1.0,1.01424872185047,359.161627591965,113.56551479447997 +2000.0,0.3,56550.0,0.483411445566456,213.536169122694,280.110696103189,84.618267061966,0.975,0.988892503804209,359.161627591965,103.22582819632473 +2000.0,0.3,55100.0,0.490485005697224,192.340298597972,280.110696103189,84.618267061966,0.95,0.963536285757948,359.161627591965,94.34003245363206 +2000.0,0.3,53650.0,0.511129913146882,168.431204556053,280.110696103189,84.618267061966,0.925,0.938180067711686,359.161627591965,86.09022695596009 +2000.0,0.3,52200.0,0.54627806233195,143.008985787908,280.110696103189,84.618267061966,0.9,0.912823849665424,359.161627591965,78.12267165227576 +2000.0,0.3,50750.0,0.612417604594913,113.852681014766,280.110696103189,84.618267061966,0.875,0.887467631619162,359.161627591965,69.72538618377173 +2000.0,0.3,49300.0,0.724501331066055,85.0427936768085,280.110696103189,84.618267061966,0.85,0.8621114135729,359.161627591965,61.61361721642364 +2000.0,0.3,47850.0,0.875007223444898,63.2213183382253,280.110696103189,84.618267061966,0.825,0.836755195526639,359.161627591965,55.31911022165654 +2000.0,0.3,46400.0,1.0468720270292,48.5772676411829,280.110696103189,84.618267061966,0.8,0.811398977480377,359.161627591965,50.85418264306511 +2000.0,0.3,44950.0,1.23898811000289,38.5602010579051,280.110696103189,84.618267061966,0.775,0.786042759434115,359.161627591965,47.77563063006528 +2000.0,0.3,43500.0,1.45886596464954,31.196860542239,280.110696103189,84.618267061966,0.75,0.760686541387853,359.161627591965,45.512038048990675 +2000.0,0.3,42050.0,1.7440198811413,24.9846441432261,280.110696103189,84.618267061966,0.725,0.735330323341591,359.161627591965,43.573716109026854 +2000.0,0.3,40600.0,2.22439955238856,18.3610232895413,280.110696103189,84.618267061966,0.7,0.70997410529533,359.161627591965,40.84225198665159 +2000.0,0.3,0.0,0.0,0.0,280.110696103189,84.618267061966,0.0,0.0,359.161627591965,0.0 +2000.0,0.35,58000.0,0.470566372455659,244.132662822051,281.902058584896,86.5248238705945,1.0,1.01102103578495,419.021898857293,114.88062154211308 +2000.0,0.35,56550.0,0.471779071681757,221.429485396085,281.902058584896,86.5248238705945,0.975,0.985745509890324,419.021898857293,104.46579706313415 +2000.0,0.35,55100.0,0.478606798245897,199.386389652093,281.902058584896,86.5248238705945,0.95,0.960469983995701,419.021898857293,95.42768156519708 +2000.0,0.35,53650.0,0.498152985137602,174.60167917058,281.902058584896,86.5248238705945,0.925,0.935194458101077,419.021898857293,86.9783476888623 +2000.0,0.35,52200.0,0.532149599633283,147.79918290782,281.902058584896,86.5248238705945,0.9,0.909918932206453,419.021898857293,78.65127601052278 +2000.0,0.35,50750.0,0.596055825341059,117.300089959081,281.902058584896,86.5248238705945,0.875,0.884643406311829,419.021898857293,69.9174019331405 +2000.0,0.35,49300.0,0.702768369468572,87.603531790668,281.902058584896,86.5248238705945,0.85,0.859367880417206,419.021898857293,61.56499119621596 +2000.0,0.35,47850.0,0.843455670817316,65.3619111562417,281.902058584896,86.5248238705945,0.825,0.834092354522582,419.021898857293,55.12987462018965 +2000.0,0.35,46400.0,1.00062754679037,50.5876558682368,281.902058584896,86.5248238705945,0.8,0.808816828627958,419.021898857293,50.61940198930926 +2000.0,0.35,44950.0,1.17751547849603,40.3045624359361,281.902058584896,86.5248238705945,0.775,0.783541302733334,419.021898857293,47.45924612232441 +2000.0,0.35,43500.0,1.38261523588192,32.6536229148696,281.902058584896,86.5248238705945,0.75,0.758265776838711,419.021898857293,45.1473965488417 +2000.0,0.35,42050.0,1.65968933222744,25.845334524983,281.902058584896,86.5248238705945,0.725,0.732990250944087,419.021898857293,42.89522599896383 +2000.0,0.35,40600.0,2.1101137137546,19.1389319939315,281.902058584896,86.5248238705945,0.7,0.707714725049463,419.021898857293,40.38532286701153 +2000.0,0.35,0.0,0.0,0.0,281.902058584896,86.5248238705945,0.0,0.0,419.021898857293,0.0 +3000.0,0.0,58000.0,0.516198592397567,195.639492579802,268.65,70.108523423274,1.0,1.03565687626109,0.0,100.98883068706806 +3000.0,0.0,56550.0,0.517518224001758,178.107075009453,268.65,70.108523423274,0.975,1.00976545435457,0.0,92.17365714104001 +3000.0,0.0,55100.0,0.526298404523301,159.542557524104,268.65,70.108523423274,0.95,0.983874032448039,0.0,83.96699347850291 +3000.0,0.0,53650.0,0.542169860110265,141.998290178002,268.65,70.108523423274,0.925,0.957982610541512,0.0,76.98719312170417 +3000.0,0.0,52200.0,0.577299820106671,122.140961867175,268.65,70.108523423274,0.9,0.932091188634984,0.0,70.5119553135759 +3000.0,0.0,50750.0,0.635607020742967,100.847615486545,268.65,70.108523423274,0.875,0.906199766728457,0.0,64.09945242843517 +3000.0,0.0,49300.0,0.74551022679156,77.029406676729,268.65,70.108523423274,0.85,0.88030834482193,0.0,57.426210441187536 +3000.0,0.0,47850.0,0.91260806276764,56.8060837234955,268.65,70.108523423274,0.825,0.854416922915402,0.0,51.84169002031559 +3000.0,0.0,46400.0,1.13593142382492,41.88698292111,268.65,70.108523423274,0.8,0.828525501008875,0.0,47.580740149306585 +3000.0,0.0,44950.0,1.38709060620493,32.2798747866438,268.65,70.108523423274,0.775,0.802634079102348,0.0,44.775111086024985 +3000.0,0.0,43500.0,1.66645574471295,25.7003648976502,268.65,70.108523423274,0.75,0.77674265719582,0.0,42.82852072490822 +3000.0,0.0,42050.0,2.00192509947777,20.6096542931463,268.65,70.108523423274,0.725,0.750851235289293,0.0,41.25898422100936 +3000.0,0.0,40600.0,2.47533674011158,16.0905941489086,268.65,70.108523423274,0.7,0.724959813382765,0.0,39.82963886701788 +3000.0,0.0,0.0,0.0,0.0,268.65,70.108523423274,0.0,0.0,0.0,0.0 +3000.0,0.05,58000.0,0.515231915059655,196.194005351224,268.784550610546,70.2313268802712,1.0,1.03539762455165,59.1509135005756,101.08541310033536 +3000.0,0.05,56550.0,0.516512621894976,178.58707151681,268.784550610546,70.2313268802712,0.975,1.00951268393785,59.1509135005756,92.24247654569312 +3000.0,0.05,55100.0,0.525172172581759,160.029535187731,268.784550610546,70.2313268802712,0.95,0.983627743324063,59.1509135005756,84.04305867178974 +3000.0,0.05,53650.0,0.541029319101805,142.406353030404,268.784550610546,70.2313268802712,0.925,0.957742802710272,59.1509135005756,77.04601221581073 +3000.0,0.05,52200.0,0.575988523705559,122.497476600987,268.784550610546,70.2313268802712,0.9,0.931857862096481,59.1509135005756,70.55714070505876 +3000.0,0.05,50750.0,0.634221898516558,101.09491519428,268.784550610546,70.2313268802712,0.875,0.90597292148269,59.1509135005756,64.11660904488669 +3000.0,0.05,49300.0,0.743690340587394,77.2205723755214,268.784550610546,70.2313268802712,0.85,0.880087980868899,59.1509135005756,57.42819377030502 +3000.0,0.05,47850.0,0.910238011377281,56.9410124794452,268.784550610546,70.2313268802712,0.825,0.854203040255108,59.1509135005756,51.82987396509914 +3000.0,0.05,46400.0,1.13211118338588,42.0112243748278,268.784550610546,70.2313268802712,0.8,0.828318099641316,59.1509135005756,47.56137694247602 +3000.0,0.05,44950.0,1.38149285186014,32.3952482558376,268.784550610546,70.2313268802712,0.775,0.802433159027525,59.1509135005756,44.75380389967431 +3000.0,0.05,43500.0,1.65940716359725,25.7917096490945,268.784550610546,70.2313268802712,0.75,0.776548218413734,59.1509135005756,42.79894775312773 +3000.0,0.05,42050.0,1.99374360102655,20.6815217588909,268.784550610546,70.2313268802712,0.725,0.750663277799943,59.1509135005756,41.23365166628009 +3000.0,0.05,40600.0,2.4653435716055,16.1429138507143,268.784550610546,70.2313268802712,0.7,0.724778337186152,59.1509135005756,39.79782888883989 +3000.0,0.05,0.0,0.0,0.0,268.784550610546,70.2313268802712,0.0,0.0,59.1509135005756,0.0 +3000.0,0.1,58000.0,0.512345912060705,197.86374949014,269.188202442184,70.6006586189506,1.0,1.0346210360193,118.301827001151,101.37468319627664 +3000.0,0.1,56550.0,0.513413933056788,180.101323052972,269.188202442184,70.6006586189506,0.975,1.00875551011882,118.301827001151,92.4665286173575 +3000.0,0.1,55100.0,0.52175065401875,161.479481606083,269.188202442184,70.6006586189506,0.95,0.982889984218339,118.301827001151,84.25202513858251 +3000.0,0.1,53650.0,0.537576876437044,143.647704717215,269.188202442184,70.6006586189506,0.925,0.957024458317857,118.301827001151,77.22168440923126 +3000.0,0.1,52200.0,0.572048276498673,123.573915217015,269.188202442184,70.6006586189506,0.9,0.931158932417374,118.301827001151,70.69024522008657 +3000.0,0.1,50750.0,0.629858992528352,101.905019044549,269.188202442184,70.6006586189506,0.875,0.905293406516891,118.301827001151,64.18579262898216 +3000.0,0.1,49300.0,0.738307215403671,77.7996067229204,269.188202442184,70.6006586189506,0.85,0.879427880616409,118.301827001151,57.44001099910008 +3000.0,0.1,47850.0,0.903148621554008,57.3434133381391,269.188202442184,70.6006586189506,0.825,0.853562354715926,118.301827001151,51.78962471154205 +3000.0,0.1,46400.0,1.12074221435107,42.3846174863529,269.188202442184,70.6006586189506,0.8,0.827696828815443,118.301827001151,47.50223005607823 +3000.0,0.1,44950.0,1.36543973089148,32.7185689600467,269.188202442184,70.6006586189506,0.775,0.801831302914961,118.301827001151,44.675233995960504 +3000.0,0.1,43500.0,1.63845504536074,26.0687311518205,269.188202442184,70.6006586189506,0.75,0.775965777014478,118.301827001151,42.71244408185299 +3000.0,0.1,42050.0,1.96772503810352,20.9056456117382,269.188202442184,70.6006586189506,0.725,0.750100251113995,118.301827001151,41.13656230793624 +3000.0,0.1,40600.0,2.43414831939933,16.3053846163222,269.188202442184,70.6006586189506,0.7,0.724234725213513,118.301827001151,39.68972456098037 +3000.0,0.1,0.0,0.0,0.0,269.188202442184,70.6006586189506,0.0,0.0,118.301827001151,0.0 +3000.0,0.15,58000.0,0.507623659124732,200.648460035772,269.860955494913,71.219289639328,1.0,1.03333059590403,177.452740501727,101.85390548110112 +3000.0,0.15,56550.0,0.508354033233313,182.639513091272,269.860955494913,71.219289639328,0.975,1.00749733100643,177.452740501727,92.84553310771658 +3000.0,0.15,55100.0,0.516147431309761,163.916869291261,269.860955494913,71.219289639328,0.95,0.981664066108829,177.452740501727,84.60527103302222 +3000.0,0.15,53650.0,0.53193673873951,145.727056071528,269.860955494913,71.219289639328,0.925,0.955830801211228,177.452740501727,77.51757495279831 +3000.0,0.15,52200.0,0.565577151470904,125.385421266134,269.860955494913,71.219289639328,0.9,0.929997536313627,177.452740501727,70.91512939567939 +3000.0,0.15,50750.0,0.622738403664192,103.254690685758,269.860955494913,71.219289639328,0.875,0.904164271416027,177.452740501727,64.30066124848885 +3000.0,0.15,49300.0,0.729483424920677,78.7676699438951,269.860955494913,71.219289639328,0.85,0.878331006518426,177.452740501727,57.45970964369407 +3000.0,0.15,47850.0,0.891377571043883,58.0225717220937,269.860955494913,71.219289639328,0.825,0.852497741620825,177.452740501727,51.72001904735938 +3000.0,0.15,46400.0,1.10212242624491,43.011546952595,269.860955494913,71.219289639328,0.8,0.826664476723224,177.452740501727,47.403990483940866 +3000.0,0.15,44950.0,1.33916696062851,33.2638563590631,269.860955494913,71.219289639328,0.775,0.800831211825623,177.452740501727,44.54585741914986 +3000.0,0.15,43500.0,1.60422105198309,26.5349844446992,269.860955494913,71.219289639328,0.75,0.774997946928023,177.452740501727,42.56798066023028 +3000.0,0.15,42050.0,1.92529334753574,21.2868541154318,269.860955494913,71.219289639328,0.725,0.749164682030422,177.452740501727,40.98343861840464 +3000.0,0.15,40600.0,2.38352766396669,16.5791994398573,269.860955494913,71.219289639328,0.7,0.723331417132821,177.452740501727,39.51698051132093 +3000.0,0.15,0.0,0.0,0.0,269.860955494913,71.219289639328,0.0,0.0,177.452740501727,0.0 +3000.0,0.2,58000.0,0.501185432900655,204.553991144178,270.802809768734,72.0918612884988,1.0,1.0315320648,236.603654002302,102.5194806031516 +3000.0,0.2,56550.0,0.501480921468596,186.226500093892,270.802809768734,72.0918612884988,0.975,1.00574376318,236.603654002302,93.38903686895655 +3000.0,0.2,55100.0,0.508504270338231,167.375177042223,270.802809768734,72.0918612884988,0.95,0.979955461559997,236.603654002302,85.11099227458783 +3000.0,0.2,53650.0,0.524269528761642,148.660587591747,270.802809768734,72.0918612884988,0.925,0.954167159939997,236.603654002302,77.93821620215401 +3000.0,0.2,52200.0,0.556766921198546,127.94229372089,270.802809768734,72.0918612884988,0.9,0.928378858319997,236.603654002302,71.23403696605999 +3000.0,0.2,50750.0,0.613143633130921,105.122312806304,270.802809768734,72.0918612884988,0.875,0.902590556699997,236.603654002302,64.45507679718237 +3000.0,0.2,49300.0,0.717431075852181,80.1280916908039,270.802809768734,72.0918612884988,0.85,0.876802255079997,236.603654002302,57.48638302771565 +3000.0,0.2,47850.0,0.875135412441549,58.9887447443547,270.802809768734,72.0918612884988,0.825,0.851013953459997,236.603654002302,51.6231394612601 +3000.0,0.2,46400.0,1.07666913219795,43.9044758421852,270.802809768734,72.0918612884988,0.8,0.825225651839997,236.603654002302,47.2705939046114 +3000.0,0.2,44950.0,1.30327148119615,34.0429892996113,270.802809768734,72.0918612884988,0.775,0.799437350219997,236.603654002302,44.36725708884911 +3000.0,0.2,43500.0,1.55759115970373,27.201651536094,270.802809768734,72.0918612884988,0.75,0.773649048599997,236.603654002302,42.369051961961404 +3000.0,0.2,42050.0,1.86760996407068,21.8302868028718,270.802809768734,72.0918612884988,0.725,0.747860746979997,236.603654002302,40.77046115156404 +3000.0,0.2,40600.0,2.3145523285331,16.9696338616178,270.802809768734,72.0918612884988,0.7,0.722072445359997,236.603654002302,39.27710556876162 +3000.0,0.2,0.0,0.0,0.0,270.802809768734,72.0918612884988,0.0,0.0,236.603654002302,0.0 +3000.0,0.25,58000.0,0.493044736925006,209.741876091979,272.013765263647,73.2249198647342,1.0,1.02923340767317,295.754567502878,103.41212811992699 +3000.0,0.25,56550.0,0.493016086818814,190.906702601767,272.013765263647,73.2249198647342,0.975,1.00350257248134,295.754567502878,94.12007546420625 +3000.0,0.25,55100.0,0.498999810156414,171.900234685622,272.013765263647,73.2249198647342,0.95,0.977771737289507,295.754567502878,85.77818447396838 +3000.0,0.25,53650.0,0.514758053908504,152.496215317938,272.013765263647,73.2249198647342,0.925,0.952040902097678,295.754567502878,78.49865502547398 +3000.0,0.25,52200.0,0.545908562471256,131.239726557535,272.013765263647,73.2249198647342,0.9,0.926310066905848,295.754567502878,71.64489046414464 +3000.0,0.25,50750.0,0.601429440402033,107.480498771677,272.013765263647,73.2249198647342,0.875,0.900579231714019,295.754567502878,64.6419362303811 +3000.0,0.25,49300.0,0.702430133887488,81.8855611304295,272.013765263647,73.2249198647342,0.85,0.87484839652219,295.754567502878,57.518885668299674 +3000.0,0.25,47850.0,0.854709778303298,60.2570342136682,272.013765263647,73.2249198647342,0.825,0.849117561330361,295.754567502878,51.50227635397859 +3000.0,0.25,46400.0,1.04496328367278,45.0775541636288,272.013765263647,73.2249198647342,0.8,0.823386726138532,295.754567502878,47.10438901876314 +3000.0,0.25,44950.0,1.25864283323665,35.0732039986435,272.013765263647,73.2249198647342,0.775,0.797655890946703,295.754567502878,44.14463685153965 +3000.0,0.25,43500.0,1.49981457910647,28.0820201747691,272.013765263647,73.2249198647342,0.75,0.771925055754874,295.754567502878,42.11782326888072 +3000.0,0.25,42050.0,1.79627203198008,22.5473720061643,272.013765263647,73.2249198647342,0.725,0.746194220563044,295.754567502878,40.50121372932352 +3000.0,0.25,40600.0,2.27207002071196,16.8772709516511,272.013765263647,73.2249198647342,0.7,0.720463385371215,295.754567502878,38.34634136067927 +3000.0,0.25,0.0,0.0,0.0,272.013765263647,73.2249198647342,0.0,0.0,295.754567502878,0.0 +3000.0,0.3,58000.0,0.483212310229862,216.394931124031,273.493821979652,74.6269652298273,1.0,1.02644469688801,354.905481003454,104.56469459047489 +3000.0,0.3,56550.0,0.483149838965041,196.705730667205,273.493821979652,74.6269652298273,0.975,1.00078357946581,354.905481003454,95.03834209536082 +3000.0,0.3,55100.0,0.488056398822271,177.459517061511,273.493821979652,74.6269652298273,0.95,0.975122462043613,354.905481003454,86.61025283378042 +3000.0,0.3,53650.0,0.503668113657407,157.246369449317,273.493821979652,74.6269652298273,0.925,0.949461344621413,354.905481003454,79.19998228001322 +3000.0,0.3,52200.0,0.533363326042695,135.270507536815,273.493821979652,74.6269652298273,0.9,0.923800227199213,354.905481003454,72.14832781531908 +3000.0,0.3,50750.0,0.587742618221576,110.365769713501,273.493821979652,74.6269652298273,0.875,0.898139109777012,354.905481003454,64.8666664534526 +3000.0,0.3,49300.0,0.684916991699412,84.0270035167196,273.493821979652,74.6269652298273,0.85,0.872477992354812,354.905481003454,57.5515224701875 +3000.0,0.3,47850.0,0.830430214476331,61.849568649524,273.493821979652,74.6269652298273,0.825,0.846816874932611,354.905481003454,51.36175055889278 +3000.0,0.3,46400.0,1.00774747838925,46.5501145190164,273.493821979652,74.6269652298273,0.8,0.821155757510411,354.905481003454,46.91076052526959 +3000.0,0.3,44950.0,1.20633964836406,36.3788583081415,273.493821979652,74.6269652298273,0.775,0.795494640088211,354.905481003454,43.885259139329385 +3000.0,0.3,43500.0,1.43234395031108,29.1971445855973,273.493821979652,74.6269652298273,0.75,0.76983352266601,354.905481003454,41.8203534135382 +3000.0,0.3,42050.0,1.72122869963321,23.2197717618068,273.493821979652,74.6269652298273,0.725,0.74417240524381,354.905481003454,39.96653755535465 +3000.0,0.3,40600.0,2.17167754759247,17.4911434746984,273.493821979652,74.6269652298273,0.7,0.71851128782161,354.905481003454,37.985123565721054 +3000.0,0.3,0.0,0.0,0.0,273.493821979652,74.6269652298273,0.0,0.0,354.905481003454,0.0 +3000.0,0.35,58000.0,0.472377875256421,224.117498662516,275.242979916748,76.3085135953898,1.0,1.02317799153118,414.056394504029,105.86814782598309 +3000.0,0.35,56550.0,0.472087318262611,203.654671541338,275.242979916748,76.3085135953898,0.975,0.997598541742899,414.056394504029,96.14278773960314 +3000.0,0.35,55100.0,0.475974871527027,184.106553281819,275.242979916748,76.3085135953898,0.95,0.97201909195462,414.056394504029,87.63009304559755 +3000.0,0.35,53650.0,0.491299920063109,162.913180056173,275.242979916748,76.3085135953898,0.925,0.94643964216634,414.056394504029,80.03923233882468 +3000.0,0.35,52200.0,0.519409889904715,140.03975178673,275.242979916748,76.3085135953898,0.9,0.920860192378061,414.056394504029,72.73803205782905 +3000.0,0.35,50750.0,0.572378922906064,113.782313899562,275.242979916748,76.3085135953898,0.875,0.895280742589781,414.056394504029,65.12659827559096 +3000.0,0.35,49300.0,0.665146477769296,86.5752299323786,275.242979916748,76.3085135953898,0.85,0.869701292801502,414.056394504029,57.58520925158856 +3000.0,0.35,47850.0,0.802663251930636,63.7955835720671,275.242979916748,76.3085135953898,0.825,0.844121843013222,414.056394504029,51.206370568768044 +3000.0,0.35,46400.0,0.965734318974126,48.3510479578558,275.242979916748,76.3085135953898,0.8,0.818542393224943,414.056394504029,46.69426637126517 +3000.0,0.35,44950.0,1.14773031129811,37.9887663411447,275.242979916748,76.3085135953898,0.775,0.792962943436663,414.056394504029,43.60085861855317 +3000.0,0.35,43500.0,1.35782527889575,30.5736206223629,275.242979916748,76.3085135953898,0.75,0.767383493648384,414.056394504029,41.51363494841276 +3000.0,0.35,42050.0,1.6277387689257,24.3404742354921,275.242979916748,76.3085135953898,0.725,0.741804043860104,414.056394504029,39.619933567147626 +3000.0,0.35,40600.0,2.0580200828216,18.2525946417168,275.242979916748,76.3085135953898,0.7,0.716224594071825,414.056394504029,37.564206336255104 +3000.0,0.35,0.0,0.0,0.0,275.242979916748,76.3085135953898,0.0,0.0,414.056394504029,0.0 +4000.0,0.0,58000.0,0.516638281867925,178.956148146694,262.15,61.6402085371994,1.0,1.04841779681353,0.0,92.45559690820986 +4000.0,0.0,56550.0,0.517698996617252,164.099830469518,262.15,61.6402085371994,0.975,1.02220735189319,0.0,84.95431757913063 +4000.0,0.0,55100.0,0.525209218898279,147.045180231338,262.15,61.6402085371994,0.95,0.995996906972856,0.0,77.22948425205769 +4000.0,0.0,53650.0,0.537282838606111,131.501113827668,262.15,61.6402085371994,0.925,0.969786462052518,0.0,70.65329171719479 +4000.0,0.0,52200.0,0.566777606055763,114.39213394041,262.15,61.6402085371994,0.9,0.94357601713218,0.0,64.83489982635578 +4000.0,0.0,50750.0,0.615639115570513,96.2801220887395,262.15,61.6402085371994,0.875,0.917365572211841,0.0,59.2738092097326 +4000.0,0.0,49300.0,0.707528060515687,75.6053666021411,262.15,61.6402085371994,0.85,0.891155127291503,0.0,53.49291839659039 +4000.0,0.0,47850.0,0.86127605780735,55.9637237486576,262.15,61.6402085371994,0.825,0.864944682371165,0.0,48.20021537046338 +4000.0,0.0,46400.0,1.08140224846311,40.784742447101,262.15,61.6402085371994,0.8,0.838734237450826,0.0,44.10471218528387 +4000.0,0.0,44950.0,1.34659370617067,30.7113471726083,262.15,61.6402085371994,0.775,0.812523792530488,0.0,41.35570681065674 +4000.0,0.0,43500.0,1.64321324926485,24.0962641608192,262.15,61.6402085371994,0.75,0.78631334761015,0.0,39.59530052684387 +4000.0,0.0,42050.0,1.99155360744515,19.2292285894136,262.15,61.6402085371994,0.725,0.760102902689811,0.0,38.296039565634075 +4000.0,0.0,40600.0,2.45997578782358,15.1243510158406,262.15,61.6402085371994,0.7,0.733892457769473,0.0,37.20553730551285 +4000.0,0.0,0.0,0.0,0.0,262.15,61.6402085371994,0.0,0.0,0.0,0.0 +4000.0,0.05,58000.0,0.515718549742496,179.461260957381,262.281303684007,61.7481857507349,1.0,1.04815533366707,58.4328508421484,92.55150123590015 +4000.0,0.05,56550.0,0.516669155633388,164.564005923171,262.281303684007,61.7481857507349,0.975,1.02195145032539,58.4328508421484,85.02514598797262 +4000.0,0.05,55100.0,0.524093387193184,147.458148552501,262.281303684007,61.7481857507349,0.95,0.995747566983717,58.4328508421484,77.28184054411595 +4000.0,0.05,53650.0,0.536108567854045,131.895209701211,262.281303684007,61.7481857507349,0.925,0.96954368364204,58.4328508421484,70.71015197972518 +4000.0,0.05,52200.0,0.565545873010277,114.716405982292,262.281303684007,61.7481857507349,0.9,0.943339800300364,58.4328508421484,64.8773899698567 +4000.0,0.05,50750.0,0.614212120107261,96.5530506540875,262.281303684007,61.7481857507349,0.875,0.917135916958687,58.4328508421484,59.30405394507085 +4000.0,0.05,49300.0,0.705841729967435,75.8030453697481,262.281303684007,61.7481857507349,0.85,0.89093203361701,58.4328508421484,53.50495268058297 +4000.0,0.05,47850.0,0.859104489425206,56.0955551086461,262.281303684007,61.7481857507349,0.825,0.864728150275333,58.4328508421484,48.191943230636916 +4000.0,0.05,46400.0,1.07814530639424,40.8877165887748,262.281303684007,61.7481857507349,0.8,0.838524266933656,58.4328508421484,44.08289972936546 +4000.0,0.05,44950.0,1.3413713649141,30.8152345613731,262.281303684007,61.7481857507349,0.775,0.81232038359198,58.4328508421484,41.33467324373718 +4000.0,0.05,43500.0,1.6363091413416,24.1796892362239,262.281303684007,61.7481857507349,0.75,0.786116500250303,58.4328508421484,39.56544653203226 +4000.0,0.05,42050.0,1.98263996649043,19.3003093545278,262.281303684007,61.7481857507349,0.725,0.759912616908626,58.4328508421484,38.265564691915934 +4000.0,0.05,40600.0,2.44952867467539,15.1753102954739,262.281303684007,61.7481857507349,0.7,0.733708733566949,58.4328508421484,37.17235771585998 +4000.0,0.05,0.0,0.0,0.0,262.281303684007,61.7481857507349,0.0,0.0,58.4328508421484,0.0 +4000.0,0.1,58000.0,0.512933719785062,180.987461006568,262.675214736028,62.0729275737883,1.0,1.04736912535132,116.865701684297,92.83457160855278 +4000.0,0.1,56550.0,0.513583714575495,165.986526452651,262.675214736028,62.0729275737883,0.975,1.02118489721753,116.865701684297,85.24797682503616 +4000.0,0.1,55100.0,0.520828743115578,148.736826411575,262.675214736028,62.0729275737883,0.95,0.995000669083752,116.865701684297,77.46641435494053 +4000.0,0.1,53650.0,0.532572195102974,133.0967600745,262.675214736028,62.0729275737883,0.925,0.968816440949969,116.865701684297,70.88363367397034 +4000.0,0.1,52200.0,0.561835821096151,115.69705645734,262.675214736028,62.0729275737883,0.9,0.942632212816186,116.865701684297,65.00275071311736 +4000.0,0.1,50750.0,0.610020844493543,97.3398208671766,262.675214736028,62.0729275737883,0.875,0.916447984682403,116.865701684297,59.379319728245264 +4000.0,0.1,49300.0,0.700814883149515,76.3816585593923,262.675214736028,62.0729275737883,0.85,0.89026375654862,116.865701684297,53.529403118066675 +4000.0,0.1,47850.0,0.852606428977874,56.4915958370926,262.675214736028,62.0729275737883,0.825,0.864079528414837,116.865701684297,48.16509779392486 +4000.0,0.1,46400.0,1.06829204463706,41.2124099743184,262.675214736028,62.0729275737883,0.8,0.837895300281054,116.865701684297,44.02688971588537 +4000.0,0.1,44950.0,1.32630913954704,31.1100782357251,262.675214736028,62.0729275737883,0.775,0.811711072147271,116.865701684297,41.26158109606566 +4000.0,0.1,43500.0,1.6157012281043,24.4363880379284,262.675214736028,62.0729275737883,0.75,0.785526844013488,116.865701684297,39.48190216331414 +4000.0,0.1,42050.0,1.95625449809703,19.514952149108,262.675214736028,62.0729275737883,0.725,0.759342615879705,116.865701684297,38.17621292184083 +4000.0,0.1,40600.0,2.4619611768912,14.8444101225498,262.675214736028,62.0729275737883,0.7,0.733158387745922,116.865701684297,36.54636141556835 +4000.0,0.1,0.0,0.0,0.0,262.675214736028,62.0729275737883,0.0,0.0,116.865701684297,0.0 +4000.0,0.15,58000.0,0.508376710865291,183.535093910855,263.331733156064,62.6168706187689,1.0,1.046062700507,175.298552526445,93.30496737075278 +4000.0,0.15,56550.0,0.508557778227161,168.353315696433,263.331733156064,62.6168706187689,0.975,1.01991113299433,175.298552526445,85.61738818775379 +4000.0,0.15,55100.0,0.515479501869442,150.886396013128,263.331733156064,62.6168706187689,0.95,0.993759565481654,175.298552526445,77.77884425572259 +4000.0,0.15,53650.0,0.526819807149964,135.101380841619,263.331733156064,62.6168706187689,0.925,0.967607997968979,175.298552526445,71.17408340067557 +4000.0,0.15,52200.0,0.555758111444936,117.342763698527,263.331733156064,62.6168706187689,0.9,0.941456430456304,175.298552526445,65.21419274482275 +4000.0,0.15,50750.0,0.603163986723064,98.6553333328164,263.331733156064,62.6168706187689,0.875,0.915304862943629,175.298552526445,59.50534416451433 +4000.0,0.15,49300.0,0.69259042623058,77.3477011375684,263.331733156064,62.6168706187689,0.85,0.889153295430954,175.298552526445,53.57027729882401 +4000.0,0.15,47850.0,0.841914795667452,57.15716419971,263.331733156064,62.6168706187689,0.825,0.863001727918279,175.298552526445,48.12146221812985 +4000.0,0.15,46400.0,1.05206836997687,41.7603409950361,263.331733156064,62.6168706187689,0.8,0.836850160405603,175.298552526445,43.93473388032589 +4000.0,0.15,44950.0,1.30150643971779,31.6106594905969,263.331733156064,62.6168706187689,0.775,0.810698592892928,175.298552526445,41.14147689073814 +4000.0,0.15,43500.0,1.58198593134005,24.8703737906756,263.331733156064,62.6168706187689,0.75,0.784547025380253,175.298552526445,39.34458144401711 +4000.0,0.15,42050.0,1.91314195786026,19.8776161794593,263.331733156064,62.6168706187689,0.725,0.758395457867578,175.298552526445,38.028701535165546 +4000.0,0.15,40600.0,2.40762299842655,15.1147622768858,263.331733156064,62.6168706187689,0.7,0.732243890354903,175.298552526445,36.390649273580294 +4000.0,0.15,0.0,0.0,0.0,263.331733156064,62.6168706187689,0.0,0.0,175.298552526445,0.0 +4000.0,0.2,58000.0,0.502168900435508,187.114078592016,264.250858944113,63.3840961442688,1.0,1.04424189146032,233.731403368594,93.9628711025559 +4000.0,0.2,56550.0,0.501751601103474,171.650832615857,264.250858944113,63.3840961442688,0.975,1.01813584417381,233.731403368594,86.12608009575068 +4000.0,0.2,55100.0,0.508189938401694,153.908131483308,264.250858944113,63.3840961442688,0.95,0.9920297968873,233.731403368594,78.21456385802212 +4000.0,0.2,53650.0,0.519052011049344,137.912781865048,264.250858944113,63.3840961442688,0.925,0.965923749600792,233.731403368594,71.58390677646265 +4000.0,0.2,52200.0,0.547479239939111,119.66833950462,264.250858944113,63.3840961442688,0.9,0.939817702314284,233.731403368594,65.51593155676484 +4000.0,0.2,50750.0,0.593837641984712,100.502751764912,264.250858944113,63.3840961442688,0.875,0.913711655027776,233.731403368594,59.6823171210502 +4000.0,0.2,49300.0,0.681342529901859,78.7155195007167,264.250858944113,63.3840961442688,0.85,0.887605607741268,233.731403368594,53.632231199157424 +4000.0,0.2,47850.0,0.827192207199755,58.1013211484694,264.250858944113,63.3840961442688,0.825,0.86149956045476,233.731403368594,48.06096008202421 +4000.0,0.2,46400.0,1.02979139836151,42.5414906100068,264.250858944113,63.3840961442688,0.8,0.835393513168252,233.731403368594,43.80886110366194 +4000.0,0.2,44950.0,1.26765700172921,32.3255071750987,264.250858944113,63.3840961442688,0.775,0.809287465881744,233.731403368594,40.97765550496168 +4000.0,0.2,43500.0,1.53603185614274,25.4910973063103,264.250858944113,63.3840961442688,0.75,0.783181418595236,233.731403368594,39.15513751052701 +4000.0,0.2,42050.0,1.86206441271866,20.2138617108206,264.250858944113,63.3840961442688,0.725,0.757075371308729,233.731403368594,37.63951253533537 +4000.0,0.2,40600.0,2.33409278241193,15.4982133670753,264.250858944113,63.3840961442688,0.7,0.730969324022221,233.731403368594,36.17426796037055 +4000.0,0.2,0.0,0.0,0.0,264.250858944113,63.3840961442688,0.0,0.0,233.731403368594,0.0 +4000.0,0.25,58000.0,0.494475248953783,191.734501623019,265.432592100177,64.3803604853571,1.0,1.04191476235198,292.164254210742,94.80796542307183 +4000.0,0.25,56550.0,0.493365083126142,175.872416132395,265.432592100177,64.3803604853571,0.975,1.01586689329318,292.164254210742,86.76930920475449 +4000.0,0.25,55100.0,0.499125808819755,157.858373934168,265.432592100177,64.3803604853571,0.95,0.989819024234382,292.164254210742,78.79118856886292 +4000.0,0.25,53650.0,0.509523423044994,141.535532357995,265.432592100177,64.3803604853571,0.925,0.963771155175582,292.164254210742,72.11566892954113 +4000.0,0.25,52200.0,0.537167924883246,122.702242161566,265.432592100177,64.3803604853571,0.9,0.937723286116782,292.164254210742,65.91170880044996 +4000.0,0.25,50750.0,0.582274081387425,102.892641336406,265.432592100177,64.3803604853571,0.875,0.911675417057983,292.164254210742,59.911718215681596 +4000.0,0.25,49300.0,0.667353118463615,80.476115534608,265.432592100177,64.3803604853571,0.85,0.885627547999183,292.164254210742,53.705986663858816 +4000.0,0.25,47850.0,0.808694238182032,59.3365047668264,265.432592100177,64.3803604853571,0.825,0.859579678940384,292.164254210742,47.98508951879319 +4000.0,0.25,46400.0,1.00186962694388,43.5736359433813,265.432592100177,64.3803604853571,0.8,0.833531809881584,292.164254210742,43.65510238718387 +4000.0,0.25,44950.0,1.22553591142411,33.2706443864228,265.432592100177,64.3803604853571,0.775,0.807483940822785,292.164254210742,40.77436949178212 +4000.0,0.25,43500.0,1.47888500326836,26.3330207379641,265.432592100177,64.3803604853571,0.75,0.781436071763985,292.164254210742,38.94350946012983 +4000.0,0.25,42050.0,1.79121279155016,20.8770024138194,265.432592100177,64.3803604853571,0.725,0.755388202705186,292.164254210742,37.39515377285688 +4000.0,0.25,40600.0,2.24542999254096,15.9862819365947,265.432592100177,64.3803604853571,0.7,0.729340333646386,292.164254210742,35.896076929645524 +4000.0,0.25,0.0,0.0,0.0,265.432592100177,64.3803604853571,0.0,0.0,292.164254210742,0.0 +4000.0,0.3,58000.0,0.485532677801374,197.394491551304,266.876932624255,65.6131378025166,1.0,1.03909151095035,350.597105052891,95.84147606614532 +4000.0,0.3,56550.0,0.48360563114096,181.054193388126,266.876932624255,65.6131378025166,0.975,1.01311422317659,350.597105052891,87.5588274641821 +4000.0,0.3,55100.0,0.488481730885005,162.804575019183,266.876932624255,65.6131378025166,0.95,0.987136935402833,350.597105052891,79.52706060136816 +4000.0,0.3,53650.0,0.498527594105241,145.971240863982,266.876932624255,65.6131378025166,0.925,0.961159647629074,350.597105052891,72.77069151647758 +4000.0,0.3,52200.0,0.524997145147183,126.483473437772,266.876932624255,65.6131378025166,0.9,0.935182359855315,350.597105052891,66.40346246312986 +4000.0,0.3,50750.0,0.568786919643188,105.83109792579,266.876932624255,65.6131378025166,0.875,0.909205072081557,350.597105052891,60.19534419166668 +4000.0,0.3,49300.0,0.651058929903836,82.6256711002017,266.876932624255,65.6131378025166,0.85,0.883227784307798,350.597105052891,53.79418100908362 +4000.0,0.3,47850.0,0.786726708471003,60.8800948269685,266.876932624255,65.6131378025166,0.825,0.857250496534039,350.597105052891,47.89599661462347 +4000.0,0.3,46400.0,0.968749806762913,44.8820304414574,266.876932624255,65.6131378025166,0.8,0.83127320876028,350.597105052891,43.47945831728904 +4000.0,0.3,44950.0,1.17618719502159,34.467582189139,266.876932624255,65.6131378025166,0.775,0.805295920986521,350.597105052891,40.54032881421952 +4000.0,0.3,43500.0,1.41427442105397,27.342512298392,266.876932624255,65.6131378025166,0.75,0.779318633212763,350.597105052891,38.6698157509694 +4000.0,0.3,42050.0,1.7082615092622,21.7155865051318,266.876932624255,65.6131378025166,0.725,0.753341345439004,350.597105052891,37.09590057777031 +4000.0,0.3,40600.0,2.14154137657687,16.6055717053225,266.876932624255,65.6131378025166,0.7,0.727364057665245,350.597105052891,35.56151888866227 +4000.0,0.3,0.0,0.0,0.0,266.876932624255,65.6131378025166,0.0,0.0,350.597105052891,0.0 +4000.0,0.35,58000.0,0.475456837401016,204.141883290552,268.583880516348,67.0916752949494,1.0,1.03578434646499,409.029955895039,97.06065421041316 +4000.0,0.35,56550.0,0.47277658919466,187.206355165262,268.583880516348,67.0916752949494,0.975,1.00988973780337,409.029955895039,88.50678207059669 +4000.0,0.35,55100.0,0.476535241522749,168.796656640343,268.583880516348,67.0916752949494,0.95,0.983995129141743,409.029955895039,80.4375555403384 +4000.0,0.35,53650.0,0.486357558404709,151.237415439444,268.583880516348,67.0916752949494,0.925,0.958100520480118,409.029955895039,73.55546011256662 +4000.0,0.35,52200.0,0.511244289102639,131.108869816639,268.583880516348,67.0916752949494,0.9,0.932205911818494,409.029955895039,67.02866094445804 +4000.0,0.35,50750.0,0.553861843741355,109.264491838343,268.583880516348,67.0916752949494,0.875,0.906311303156869,409.029955895039,60.517432905046896 +4000.0,0.35,49300.0,0.63294534978744,85.1316129863787,268.583880516348,67.0916752949494,0.85,0.880416694495244,409.029955895039,53.88365855963244 +4000.0,0.35,47850.0,0.761655798536187,62.752862602764,268.583880516348,67.0916752949494,0.825,0.854522085833619,409.029955895039,47.79608167613984 +4000.0,0.35,46400.0,0.930928272769655,46.509216683074,268.583880516348,67.0916752949494,0.8,0.828627477171994,409.029955895039,43.2967447546437 +4000.0,0.35,44950.0,1.12092406496459,35.9535859074632,268.583880516348,67.0916752949494,0.775,0.802732868510369,409.029955895039,40.30123966544725 +4000.0,0.35,43500.0,1.34214772117032,28.5745316319166,268.583880516348,67.0916752949494,0.75,0.776838259848744,409.029955895039,38.351242513286095 +4000.0,0.35,42050.0,1.61627594977978,22.7349539579851,268.583880516348,67.0916752949494,0.725,0.75094365118712,409.029955895039,36.74595930164193 +4000.0,0.35,40600.0,2.02665080752162,17.3504035569554,268.583880516348,67.0916752949494,0.7,0.725049042525495,409.029955895039,35.16320937952965 +4000.0,0.35,0.0,0.0,0.0,268.583880516348,67.0916752949494,0.0,0.0,409.029955895039,0.0 +5000.0,0.0,58000.0,0.518246696506667,162.250085387309,255.65,54.019881314929,1.0,1.06166235198407,0.0,84.08557075989754 +5000.0,0.0,56550.0,0.520357994792976,149.523156804776,255.65,54.019881314929,0.975,1.03512079318447,0.0,77.80557005004896 +5000.0,0.0,55100.0,0.524609481589099,135.569262000759,255.65,54.019881314929,0.95,1.00857923438487,0.0,71.12092025763492 +5000.0,0.0,53650.0,0.5368267262803,120.847121314339,255.65,54.019881314929,0.925,0.982037675585263,0.0,64.87396451557488 +5000.0,0.0,52200.0,0.559100659317848,106.631628146617,255.65,54.019881314929,0.9,0.955496116785662,0.0,59.617813600909166 +5000.0,0.0,50750.0,0.60284323239843,90.714670469783,255.65,54.019881314929,0.875,0.92895455798606,0.0,54.68672517196239 +5000.0,0.0,49300.0,0.678084139353396,73.4642188139315,255.65,54.019881314929,0.85,0.902412999186458,0.0,49.814921587714295 +5000.0,0.0,47850.0,0.815852899049915,55.1401506458041,255.65,54.019881314929,0.825,0.875871440386856,0.0,44.98625175842832 +5000.0,0.0,46400.0,1.02765242307153,40.0283700761439,255.65,54.019881314929,0.8,0.849329881587255,0.0,41.135251500353206 +5000.0,0.0,44950.0,1.30750823132573,29.4222992632827,255.65,54.019881314929,0.775,0.822788322787653,0.0,38.469898471271094 +5000.0,0.0,43500.0,1.62754845803272,22.6782115758849,255.65,54.019881314929,0.75,0.796246763988051,0.0,36.90988828127125 +5000.0,0.0,42050.0,2.00920504969854,17.7874094625612,255.65,54.019881314929,0.725,0.76970520518845,0.0,35.73855291323356 +5000.0,0.0,40600.0,2.53137262768456,13.7163011931135,255.65,54.019881314929,0.7,0.743163646388848,0.0,34.72106939332458 +5000.0,0.0,0.0,0.0,0.0,255.65,54.019881314929,0.0,0.0,0.0,0.0 +5000.0,0.05,58000.0,0.516752133377032,163.140244444615,255.778056334361,54.1145158994384,1.0,1.06139655590359,57.7057583324503,84.30306935640529 +5000.0,0.05,56550.0,0.519348214207269,149.959118413857,255.778056334361,54.1145158994384,0.975,1.034861642006,57.7057583324503,77.88100035233302 +5000.0,0.05,55100.0,0.52354921624391,135.9323215579,255.778056334361,54.1145158994384,0.95,1.00832672810841,57.7057583324503,71.1672604138537 +5000.0,0.05,53650.0,0.535656256196874,121.20968523407,255.778056334361,54.1145158994384,0.925,0.981791814210823,57.7057583324503,64.92672620728345 +5000.0,0.05,52200.0,0.557879248119083,106.935379836636,255.778056334361,54.1145158994384,0.9,0.955256900313234,57.7057583324503,59.657029300591034 +5000.0,0.05,50750.0,0.601373549085895,90.988630071994,255.778056334361,54.1145158994384,0.875,0.928721986415644,57.7057583324503,54.71815539285863 +5000.0,0.05,49300.0,0.676409044927178,73.6675888993647,255.778056334361,54.1145158994384,0.85,0.902187072518054,57.7057583324503,49.82942344950726 +5000.0,0.05,47850.0,0.813876312979595,55.2611344518302,255.778056334361,54.1145158994384,0.825,0.875652158620464,57.7057583324503,44.975728358725235 +5000.0,0.05,46400.0,1.02479297251044,40.124902133305,255.778056334361,54.1145158994384,0.8,0.849117244722874,57.7057583324503,41.11971772888013 +5000.0,0.05,44950.0,1.30272446539547,29.5149982537354,255.778056334361,54.1145158994384,0.775,0.822582330825284,57.7057583324503,38.449910321245675 +5000.0,0.05,43500.0,1.62097077598601,22.7534387908991,255.778056334361,54.1145158994384,0.75,0.796047416927694,57.7057583324503,36.882659333233896 +5000.0,0.05,42050.0,2.00014863712078,17.8538244868514,255.778056334361,54.1145158994384,0.725,0.769512503030105,57.7057583324503,35.71030271476944 +5000.0,0.05,40600.0,2.51826202008928,13.7757528719333,255.778056334361,54.1145158994384,0.7,0.742977589132515,57.7057583324503,34.69095525552545 +5000.0,0.05,0.0,0.0,0.0,255.778056334361,54.1145158994384,0.0,0.0,57.7057583324503,0.0 +5000.0,0.1,58000.0,0.514061195005615,164.53000982987,256.162225337444,54.3991297683344,1.0,1.0606003638621,115.411516664901,84.57849346742856 +5000.0,0.1,56550.0,0.516403351526352,151.233341263994,256.162225337444,54.3991297683344,0.975,1.03408535476554,115.411516664901,78.09740429125505 +5000.0,0.1,55100.0,0.52037921011584,137.069207099578,256.162225337444,54.3991297683344,0.95,1.00757034566899,115.411516664901,71.32796572168289 +5000.0,0.1,53650.0,0.532100729294331,122.302942562597,256.162225337444,54.3991297683344,0.925,0.981055336572438,115.411516664901,65.07748493240054 +5000.0,0.1,52200.0,0.554240267534791,107.855611882432,256.162225337444,54.3991297683344,0.9,0.954540327475886,115.411516664901,59.7779231848477 +5000.0,0.1,50750.0,0.597131775319073,91.7763464848068,256.162225337444,54.3991297683344,0.875,0.928025318379333,115.411516664901,54.802572708771045 +5000.0,0.1,49300.0,0.671621908208454,74.2457866475003,256.162225337444,54.3991297683344,0.85,0.901510309282781,115.411516664901,49.865096904631905 +5000.0,0.1,47850.0,0.80776230960516,55.6637293383272,256.162225337444,54.3991297683344,0.825,0.874995300186229,115.411516664901,44.96306257156369 +5000.0,0.1,46400.0,1.016187976604,40.4183748079322,256.162225337444,54.3991297683344,0.8,0.848480291089676,115.411516664901,41.07266651369471 +5000.0,0.1,44950.0,1.28869709034324,29.7833759864807,256.162225337444,54.3991297683344,0.775,0.821965281993124,115.411516664901,38.381749974376405 +5000.0,0.1,43500.0,1.60067732113231,22.9914708974933,256.162225337444,54.3991297683344,0.75,0.795450272896571,115.411516664901,36.801926045091044 +5000.0,0.1,42050.0,1.97378083969854,18.0491697188451,256.162225337444,54.3991297683344,0.725,0.768935263800019,115.411516664901,35.62510536352354 +5000.0,0.1,40600.0,2.48476694708568,13.9231822780183,256.162225337444,54.3991297683344,0.7,0.742420254703466,115.411516664901,34.59586312266898 +5000.0,0.1,0.0,0.0,0.0,256.162225337444,54.3991297683344,0.0,0.0,115.411516664901,0.0 +5000.0,0.15,58000.0,0.509665687846681,166.847026869279,256.80250700925,54.8758585840331,1.0,1.05927734954021,173.117274997351,85.03620471450475 +5000.0,0.15,56550.0,0.511582403647968,153.361047009971,256.80250700925,54.8758585840331,0.975,1.0327954158017,173.117274997351,78.45681305532999 +5000.0,0.15,55100.0,0.515201040467881,138.97416770803,256.80250700925,54.8758585840331,0.95,1.0063134820632,173.117274997351,71.59963580133484 +5000.0,0.15,53650.0,0.526284008552761,124.139105285384,256.80250700925,54.8758585840331,0.925,0.979831548324692,173.117274997351,65.33242594774514 +5000.0,0.15,52200.0,0.548288180075291,109.396571893106,256.80250700925,54.8758585840331,0.9,0.953349614586187,173.117274997351,59.98084730974682 +5000.0,0.15,50750.0,0.590209970057099,93.0925277401187,256.80250700925,54.8758585840331,0.875,0.926867680847682,173.117274997351,54.94413801003511 +5000.0,0.15,49300.0,0.663809295181086,75.2082030440106,256.80250700925,54.8758585840331,0.85,0.900385747109176,173.117274997351,49.92390425448068 +5000.0,0.15,47850.0,0.797710521235381,56.3395856202963,256.80250700925,54.8758585840331,0.825,0.873903813370671,173.117274997351,44.942680211351934 +5000.0,0.15,46400.0,1.00197401193305,40.9148193761549,256.80250700925,54.8758585840331,0.8,0.847421879632166,173.117274997351,40.99558571784202 +5000.0,0.15,44950.0,1.26567357092106,30.2369737876224,256.80250700925,54.8758585840331,0.775,0.820939945893661,173.117274997351,38.27013858762653 +5000.0,0.15,43500.0,1.56731641897143,23.4118194340459,256.80250700925,54.8758585840331,0.75,0.794458012155155,173.117274997351,36.69372899697455 +5000.0,0.15,42050.0,1.9298587367802,18.3867953502695,256.80250700925,54.8758585840331,0.725,0.76797607841665,173.117274997351,35.48391764810716 +5000.0,0.15,40600.0,2.42799850768839,14.1835499643512,256.80250700925,54.8758585840331,0.7,0.741494144678145,173.117274997351,34.437638147168435 +5000.0,0.15,0.0,0.0,0.0,256.80250700925,54.8758585840331,0.0,0.0,173.117274997351,0.0 +5000.0,0.2,58000.0,0.503682619646487,170.103179946096,257.698901349778,55.5482795233554,1.0,1.05743341970528,230.823033329801,85.6780152854474 +5000.0,0.2,56550.0,0.505010022911426,156.34762318493,257.698901349778,55.5482795233554,0.975,1.03099758421265,230.823033329801,78.95711676676851 +5000.0,0.2,55100.0,0.508167038876388,141.670991185465,257.698901349778,55.5482795233554,0.95,1.00456174872002,230.823033329801,71.99252808540062 +5000.0,0.2,53650.0,0.518368571739874,126.736775213638,257.698901349778,55.5482795233554,0.925,0.978125913227389,230.823033329801,65.696361154411 +5000.0,0.2,52200.0,0.540193858126767,111.573229634416,257.698901349778,55.5482795233554,0.9,0.951690077734756,230.823033329801,60.27117337987891 +5000.0,0.2,50750.0,0.58082043821956,94.9414649814655,257.698901349778,55.5482795233554,0.875,0.925254242242124,230.823033329801,55.1439432957418 +5000.0,0.2,49300.0,0.653209833004123,76.5545862467353,257.698901349778,55.5482795233554,0.85,0.898818406749492,230.823033329801,50.0062084979297 +5000.0,0.2,47850.0,0.783910839193015,57.2999926042574,257.698901349778,55.5482795233554,0.825,0.87238257125686,230.823033329801,44.918085288156966 +5000.0,0.2,46400.0,0.982401190970184,41.6218796168076,257.698901349778,55.5482795233554,0.8,0.845946735764228,230.823033329801,40.889384105969405 +5000.0,0.2,44950.0,1.23436798291207,30.8845483361624,257.698901349778,55.5482795233554,0.775,0.819510900271596,230.823033329801,38.12289763285911 +5000.0,0.2,43500.0,1.5231168603958,23.9732411175547,257.698901349778,55.5482795233554,0.75,0.793075064778964,230.823033329801,36.51404774448142 +5000.0,0.2,42050.0,1.87093761994391,18.8588917074703,257.698901349778,55.5482795233554,0.725,0.766639229286331,230.823033329801,35.283809965954426 +5000.0,0.2,40600.0,2.35143380383289,14.5519079497807,257.698901349778,55.5482795233554,0.7,0.740203393793699,230.823033329801,34.217848263378905 +5000.0,0.2,0.0,0.0,0.0,257.698901349778,55.5482795233554,0.0,0.0,230.823033329801,0.0 +5000.0,0.25,58000.0,0.496269569012698,174.310139200679,258.851408359027,56.4214379510871,1.0,1.05507674141852,288.528791662252,86.50481765566435 +5000.0,0.25,56550.0,0.496464296105077,160.450868675724,258.851408359027,56.4214379510871,0.975,1.02869982288306,288.528791662252,79.65812757654146 +5000.0,0.25,55100.0,0.499446311519522,145.189820087766,258.851408359027,56.4214379510871,0.95,1.00232290434759,288.528791662252,72.51452011301774 +5000.0,0.25,53650.0,0.508581518588864,130.117500739234,258.851408359027,56.4214379510871,0.925,0.97594598581213,288.528791662252,66.17535612094727 +5000.0,0.25,52200.0,0.53013046932598,114.426528276526,258.851408359027,56.4214379510871,0.9,0.949569067276667,288.528791662252,60.66098913857724 +5000.0,0.25,50750.0,0.569241427319018,97.3287721396932,258.851408359027,56.4214379510871,0.875,0.923192148741204,288.528791662252,55.40356917200643 +5000.0,0.25,49300.0,0.640269463105119,78.2384621958888,258.851408359027,56.4214379510871,0.85,0.896815230205741,288.528791662252,50.09369818433187 +5000.0,0.25,47850.0,0.766549266745674,58.566909883541,258.851408359027,56.4214379510871,0.825,0.870438311670278,288.528791662252,44.894421826788324 +5000.0,0.25,46400.0,0.957751001746161,42.5542026670322,258.851408359027,56.4214379510871,0.8,0.844061393134815,288.528791662252,40.75633023285925 +5000.0,0.25,44950.0,1.19568715192549,31.7336567136248,258.851408359027,56.4214379510871,0.775,0.817684474599352,288.528791662252,37.943525616095236 +5000.0,0.25,43500.0,1.46878284327374,24.7023528818067,258.851408359027,56.4214379510871,0.75,0.791307556063889,288.528791662252,36.28239210129131 +5000.0,0.25,42050.0,1.79847970184233,19.476505362586,258.851408359027,56.4214379510871,0.725,0.764930637528426,288.528791662252,35.02809955743421 +5000.0,0.25,40600.0,2.25744822074899,15.0341447328158,258.851408359027,56.4214379510871,0.7,0.738553718992963,288.528791662252,33.938803277577826 +5000.0,0.25,0.0,0.0,0.0,258.851408359027,56.4214379510871,0.0,0.0,288.528791662252,0.0 +5000.0,0.3,58000.0,0.487662021753793,179.468275106696,260.260028036999,57.5018848914076,1.0,1.0522176425886,346.234549994702,87.5198618791973 +5000.0,0.3,56550.0,0.486415677723434,165.5637012311,260.260028036999,57.5018848914076,0.975,1.02591220152388,346.234549994702,80.53277994072566 +5000.0,0.3,55100.0,0.489294715862642,149.53961475835,260.260028036999,57.5018848914076,0.95,0.999606760459169,346.234549994702,73.16894331339581 +5000.0,0.3,53650.0,0.497210226659682,134.310029846869,260.260028036999,57.5018848914076,0.925,0.973301319394454,346.234549994702,66.7803203828304 +5000.0,0.3,52200.0,0.518382091033499,117.966171682858,260.260028036999,57.5018848914076,0.9,0.946995878329739,346.234549994702,61.15155074817667 +5000.0,0.3,50750.0,0.555831972579993,100.251942339675,260.260028036999,57.5018848914076,0.875,0.920690437265024,346.234549994702,55.72323486563727 +5000.0,0.3,49300.0,0.625154691193909,80.306424242145,260.260028036999,57.5018848914076,0.85,0.894384996200309,346.234549994702,50.2039378479852 +5000.0,0.3,47850.0,0.746160611162508,60.1269160590895,260.260028036999,57.5018848914076,0.825,0.868079555135594,346.234549994702,44.86433643396704 +5000.0,0.3,46400.0,0.928393195741348,43.7297595932427,260.260028036999,57.5018848914076,0.8,0.841774114070879,346.234549994702,40.59841125777146 +5000.0,0.3,44950.0,1.14915440809038,32.861443769403,260.260028036999,57.5018848914076,0.775,0.815468673006164,346.234549994702,37.76287296382361 +5000.0,0.3,43500.0,1.40495722142123,25.626168079065,260.260028036999,57.5018848914076,0.75,0.789163231941449,346.234549994702,36.003669900036584 +5000.0,0.3,42050.0,1.71394150336082,20.2586663607693,260.260028036999,57.5018848914076,0.725,0.762857790876734,346.234549994702,34.72216907846221 +5000.0,0.3,40600.0,2.14906839885651,15.6355707659906,260.260028036999,57.5018848914076,0.7,0.736552349812019,346.234549994702,33.60191103127507 +5000.0,0.3,0.0,0.0,0.0,260.260028036999,57.5018848914076,0.0,0.0,346.234549994702,0.0 +5000.0,0.35,58000.0,0.477992679080351,185.614795451357,261.924760383694,58.7977254249342,1.0,1.04886848821832,403.940308327152,88.72251335474547 +5000.0,0.35,56550.0,0.475370462771992,171.484393631987,261.924760383694,58.7977254249342,0.975,1.02264677601286,403.940308327152,81.51861555901209 +5000.0,0.35,55100.0,0.477907872516205,154.761344679709,261.924760383694,58.7977254249342,0.95,0.996425063807402,403.940308327152,73.96166498362683 +5000.0,0.35,53650.0,0.484659200087829,139.314594418563,261.924760383694,58.7977254249342,0.925,0.970203351601944,403.940308327152,67.52009989146106 +5000.0,0.35,52200.0,0.505291171575943,122.181028947528,261.924760383694,58.7977254249342,0.9,0.943981639396486,403.940308327152,61.73699526125064 +5000.0,0.35,50750.0,0.541001583571571,103.693233504967,261.924760383694,58.7977254249342,0.875,0.917759927191029,403.940308327152,56.09820353184383 +5000.0,0.35,49300.0,0.608169538484932,82.7604256728757,261.924760383694,58.7977254249342,0.85,0.891538214985571,403.940308327152,50.332369886289335 +5000.0,0.35,47850.0,0.723175117926988,61.989703384996,261.924760383694,58.7977254249342,0.825,0.865316502780113,403.940308327152,44.82941105570349 +5000.0,0.35,46400.0,0.894678133453339,45.1829233542759,261.924760383694,58.7977254249342,0.8,0.839094790574655,403.940308327152,40.424173530568844 +5000.0,0.35,44950.0,1.09794357801275,34.181649826939,261.924760383694,58.7977254249342,0.775,0.812873078369197,403.940308327152,37.529522913368304 +5000.0,0.35,43500.0,1.33244623474888,26.7778106171551,261.924760383694,58.7977254249342,0.75,0.786651366163739,403.940308327152,35.67999293164689 +5000.0,0.35,42050.0,1.61977017962671,21.218800450405,261.924760383694,58.7977254249342,0.725,0.760429653958281,403.940308327152,34.36958021701582 +5000.0,0.35,40600.0,2.0294520043276,16.3628368561968,261.924760383694,58.7977254249342,0.7,0.734207941752823,403.940308327152,33.207592054294125 +5000.0,0.35,0.0,0.0,0.0,261.924760383694,58.7977254249342,0.0,0.0,403.940308327152,0.0 +6000.0,0.0,58000.0,0.519421974629505,146.994605171756,249.15,47.1809940960953,1.0,1.07542188384738,0.0,76.35222807819795 +6000.0,0.0,56550.0,0.522390341017192,136.109005418484,249.15,47.1809940960953,0.975,1.04853633675119,0.0,71.10202975607268 +6000.0,0.0,55100.0,0.526598698087967,124.487524313818,249.15,47.1809940960953,0.95,1.02165078965501,0.0,65.5549682318507 +6000.0,0.0,53650.0,0.537728139104616,111.096684704469,249.15,47.1809940960953,0.925,0.994765242558824,0.0,59.739813526826374 +6000.0,0.0,52200.0,0.55634468508304,98.5370676395621,249.15,47.1809940960953,0.9,0.967879695462639,0.0,54.82057386493839 +6000.0,0.0,50750.0,0.594812343494651,84.8301673978975,249.15,47.1809940960953,0.875,0.940994148366455,0.0,50.45803066898695 +6000.0,0.0,49300.0,0.658749085046626,70.33002629933,249.15,47.1809940960953,0.85,0.91410860127027,0.0,46.32984047598878 +6000.0,0.0,47850.0,0.777981532592966,54.210008964547,249.15,47.1809940960953,0.825,0.887223054174086,0.0,42.1743858561167 +6000.0,0.0,46400.0,0.978919894518656,39.4580930582098,249.15,47.1809940960953,0.8,0.860337507077901,0.0,38.626312294450045 +6000.0,0.0,44950.0,1.27203213569009,28.4183197959078,249.15,47.1809940960953,0.775,0.833451959981717,0.0,36.149016022712566 +6000.0,0.0,43500.0,1.62309360609651,21.4650660149825,249.15,47.1809940960953,0.75,0.806566412885532,0.0,34.839811403357594 +6000.0,0.0,42050.0,2.04901075732315,16.6026403188136,249.15,47.1809940960953,0.725,0.779680865789348,0.0,34.018988613216116 +6000.0,0.0,40600.0,2.61365991407012,12.8016103699634,249.15,47.1809940960953,0.7,0.752795318693164,0.0,33.459055859517704 +6000.0,0.0,0.0,0.0,0.0,249.15,47.1809940960953,0.0,0.0,0.0,0.0 +6000.0,0.05,58000.0,0.51854347292176,147.411856458091,249.274808561608,47.2636533672839,1.0,1.07515262546503,56.9692902316657,76.43945599762247 +6000.0,0.05,56550.0,0.521394409909503,136.501385666879,249.274808561608,47.2636533672839,0.975,1.04827380982841,56.9692902316657,71.17105943161187 +6000.0,0.05,55100.0,0.52550481611347,124.8544335046,249.274808561608,47.2636533672839,0.95,1.02139499419178,56.9692902316657,65.61160611978629 +6000.0,0.05,53650.0,0.536557136616859,111.410600251112,249.274808561608,47.2636533672839,0.925,0.994516178555157,56.9692902316657,59.77815265950217 +6000.0,0.05,52200.0,0.555067283055608,98.8345736352427,249.274808561608,47.2636533672839,0.9,0.967637362918531,56.9692902316657,54.8598382596736 +6000.0,0.05,50750.0,0.593392260774357,85.0864123936198,249.274808561608,47.2636533672839,0.875,0.940758547281905,56.9692902316657,50.48961861142932 +6000.0,0.05,49300.0,0.657195368081725,70.5162165438766,249.274808561608,47.2636533672839,0.85,0.913879731645279,56.9692902316657,46.3429308872836 +6000.0,0.05,47850.0,0.776029279771654,54.3441851185059,249.274808561608,47.2636533672839,0.825,0.887000916008653,56.9692902316657,42.17267883729157 +6000.0,0.05,46400.0,0.976289906106596,39.5509834458122,249.274808561608,47.2636533672839,0.8,0.860122100372027,56.9692902316657,38.61322591473553 +6000.0,0.05,44950.0,1.26773469405731,28.4966659519572,249.274808561608,47.2636533672839,0.775,0.833243284735401,56.9692902316657,36.12621209225782 +6000.0,0.05,43500.0,1.61654797284725,21.5353334936601,249.274808561608,47.2636533672839,0.75,0.806364469098775,56.9692902316657,34.81289970376572 +6000.0,0.05,42050.0,2.03949511551869,16.6648825708255,249.274808561608,47.2636533672839,0.725,0.77948565346215,56.9692902316657,33.98794660389115 +6000.0,0.05,40600.0,2.60080971040092,12.8514909917473,249.274808561608,47.2636533672839,0.7,0.752606837825524,56.9692902316657,33.424282564466324 +6000.0,0.05,0.0,0.0,0.0,249.274808561608,47.2636533672839,0.0,0.0,56.9692902316657,0.0 +6000.0,0.1,58000.0,0.51593294559035,148.664911279609,249.649234246432,47.5122514767502,1.0,1.07434606217843,113.938580463331,76.70112558241671 +6000.0,0.1,56550.0,0.518512989274105,137.666796989054,249.649234246432,47.5122514767502,0.975,1.04748741062397,113.938580463331,71.38202243058576 +6000.0,0.1,55100.0,0.522302392234581,125.935010293652,249.649234246432,47.5122514767502,0.95,1.02062875906951,113.938580463331,65.77615714246102 +6000.0,0.1,53650.0,0.53316050953024,112.356015049348,249.649234246432,47.5122514767502,0.925,0.993770107515046,113.938580463331,59.90379023249769 +6000.0,0.1,52200.0,0.551267581796574,99.7361184988789,249.649234246432,47.5122514767502,0.9,0.966911455960585,113.938580463331,54.981288862653514 +6000.0,0.1,50750.0,0.589339234790663,85.8055511150101,249.649234246432,47.5122514767502,0.875,0.940052804406124,113.938580463331,50.568577834911174 +6000.0,0.1,49300.0,0.652501701367449,71.0768539446014,249.649234246432,47.5122514767502,0.85,0.913194152851664,113.938580463331,46.37776812669809 +6000.0,0.1,47850.0,0.770183251363122,54.7559110875161,249.649234246432,47.5122514767502,0.825,0.886335501297203,113.938580463331,42.17208563273317 +6000.0,0.1,46400.0,0.968351353235641,39.8342696780011,249.649234246432,47.5122514767502,0.8,0.859476849742742,113.938580463331,38.57356894784583 +6000.0,0.1,44950.0,1.25516810155459,28.7329702282286,249.649234246432,47.5122514767502,0.775,0.832618198188282,113.938580463331,36.06470769339024 +6000.0,0.1,43500.0,1.59732133956316,21.7421347968869,249.649234246432,47.5122514767502,0.75,0.805759546633821,113.938580463331,34.72917587872618 +6000.0,0.1,42050.0,2.01219775433977,16.8441208754777,249.649234246432,47.5122514767502,0.725,0.77890089507936,113.938580463331,33.893702199463874 +6000.0,0.1,40600.0,2.56349539615701,12.9977845937861,249.649234246432,47.5122514767502,0.7,0.752042243524899,113.938580463331,33.31976096641118 +6000.0,0.1,0.0,0.0,0.0,249.649234246432,47.5122514767502,0.0,0.0,113.938580463331,0.0 +6000.0,0.15,58000.0,0.511666632756061,150.758337253728,250.273277054471,47.9286539563753,1.0,1.07300581445337,170.907870694997,77.13801078251763 +6000.0,0.15,56550.0,0.513801571809086,139.612977518912,250.273277054471,47.9286539563753,0.975,1.04618066909203,170.907870694997,71.73336729416359 +6000.0,0.15,55100.0,0.517087747546741,127.728380060473,250.273277054471,47.9286539563753,0.95,1.0193555237307,170.907870694997,66.04678034326405 +6000.0,0.15,53650.0,0.527594177261484,113.945886755664,250.273277054471,47.9286539563753,0.925,0.992530378369365,170.907870694997,60.11718637518479 +6000.0,0.15,52200.0,0.54508784018803,101.239779239551,250.273277054471,47.9286539563753,0.9,0.965705233008031,170.907870694997,55.18457260679981 +6000.0,0.15,50750.0,0.582704078485421,87.0124366179705,250.273277054471,47.9286539563753,0.875,0.938880087646697,170.907870694997,50.702501696245605 +6000.0,0.15,49300.0,0.644917072832505,72.0152639667671,250.273277054471,47.9286539563753,0.85,0.912054942285362,170.907870694997,46.4438732367076 +6000.0,0.15,47850.0,0.760616440723587,55.443324431916,250.273277054471,47.9286539563753,0.825,0.885229796924028,170.907870694997,42.17110409128704 +6000.0,0.15,46400.0,0.955247121055815,40.3125431622509,250.273277054471,47.9286539563753,0.8,0.858404651562694,170.907870694997,38.50844079817845 +6000.0,0.15,44950.0,1.23443206088574,29.1317686919852,250.273277054471,47.9286539563753,0.775,0.83157950620136,170.907870694997,35.96118926369397 +6000.0,0.15,43500.0,1.56572815453965,22.0929485185965,250.273277054471,47.9286539563753,0.75,0.804754360840025,170.907870694997,34.59155151236159 +6000.0,0.15,42050.0,1.9670599793759,17.1517038027952,250.273277054471,47.9286539563753,0.725,0.777929215478691,170.907870694997,33.73843012858787 +6000.0,0.15,40600.0,2.50279438630843,13.2438941140896,250.273277054471,47.9286539563753,0.7,0.751104070117357,170.907870694997,33.14674384160671 +6000.0,0.15,0.0,0.0,0.0,250.273277054471,47.9286539563753,0.0,0.0,170.907870694997,0.0 +6000.0,0.2,58000.0,0.505864891457361,153.700777797242,251.146936985726,48.5159855226555,1.0,1.07113786638299,227.877160926663,77.7518272773138 +6000.0,0.2,56550.0,0.507392989345275,142.345605149098,251.146936985726,48.5159855226555,0.975,1.04435941972341,227.877160926663,72.225162116763 +6000.0,0.2,55100.0,0.510034086417718,130.224684851585,251.146936985726,48.5159855226555,0.95,1.01758097306384,227.877160926663,66.4190281673134 +6000.0,0.2,53650.0,0.519990518043506,116.203031514792,251.146936985726,48.5159855226555,0.925,0.990802526404261,227.877160926663,60.42447455560255 +6000.0,0.2,52200.0,0.536745159875203,103.346528724363,251.146936985726,48.5159855226555,0.9,0.964024079744687,227.877160926663,55.470749082705474 +6000.0,0.2,50750.0,0.573647549349413,88.7218749112501,251.146936985726,48.5159855226555,0.875,0.937245633085112,227.877160926663,50.895086116523785 +6000.0,0.2,49300.0,0.634591892855428,73.3328971215598,251.146936985726,48.5159855226555,0.85,0.910467186425537,227.877160926663,46.536461992943 +6000.0,0.2,47850.0,0.747581910092109,56.4074168636116,251.146936985726,48.5159855226555,0.825,0.883688739765963,227.877160926663,42.1691644422606 +6000.0,0.2,46400.0,0.937205126358831,40.9940513762024,251.146936985726,48.5159855226555,0.8,0.856910293106388,227.877160926663,38.41983509999418 +6000.0,0.2,44950.0,1.20460278836965,29.7646651348364,251.146936985726,48.5159855226555,0.775,0.830131846446813,227.877160926663,35.85459861631283 +6000.0,0.2,43500.0,1.5225040511401,22.5956859684225,251.146936985726,48.5159855226555,0.75,0.803353399787239,227.877160926663,34.402023425212775 +6000.0,0.2,42050.0,1.90603560984566,17.5881147407626,251.146936985726,48.5159855226555,0.725,0.776574953127664,227.877160926663,33.52357300594488 +6000.0,0.2,40600.0,2.42080085784825,13.5934340955407,251.146936985726,48.5159855226555,0.7,0.749796506468089,227.877160926663,32.90699691958858 +6000.0,0.2,0.0,0.0,0.0,251.146936985726,48.5159855226555,0.0,0.0,227.877160926663,0.0 +6000.0,0.25,58000.0,0.498686027959403,157.503529780503,252.270214040197,49.2786533779784,1.0,1.0687504919372,284.846451158329,78.54480965582457 +6000.0,0.25,56550.0,0.499450656659723,145.875853827004,252.270214040197,49.2786533779784,0.975,1.04203172963877,284.846451158329,72.85779098469492 +6000.0,0.25,55100.0,0.501350186123495,133.417788537055,252.270214040197,49.2786533779784,0.95,1.01531296734034,284.846451158329,66.88903311523762 +6000.0,0.25,53650.0,0.510527450271479,119.160385753063,252.270214040197,49.2786533779784,0.925,0.988594205041908,284.846451158329,60.83464791187713 +6000.0,0.25,52200.0,0.526561585642671,106.045392962282,252.270214040197,49.2786533779784,0.9,0.961875442743478,284.846451158329,55.83943026831935 +6000.0,0.25,50750.0,0.56235489453584,90.9591973628607,252.270214040197,49.2786533779784,0.875,0.935156680445049,284.846451158329,51.151349840056184 +6000.0,0.25,49300.0,0.621788633876176,75.0338018194786,252.270214040197,49.2786533779784,0.85,0.908437918146618,284.846451158329,46.655165127869324 +6000.0,0.25,47850.0,0.731391381819095,57.6554094288205,252.270214040197,49.2786533779784,0.825,0.881719155848188,284.846451158329,42.1686695714907 +6000.0,0.25,46400.0,0.914474868477157,41.8936517333171,252.270214040197,49.2786533779784,0.8,0.855000393549759,284.846451158329,38.310691658852974 +6000.0,0.25,44950.0,1.16852411481809,30.5328814412811,252.270214040197,49.2786533779784,0.775,0.828281631251329,284.846451158329,35.67840825901869 +6000.0,0.25,43500.0,1.46795912647773,23.2706495400573,252.270214040197,49.2786533779784,0.75,0.801562868952899,284.846451158329,34.160362371391905 +6000.0,0.25,42050.0,1.83063475960272,18.1640745685555,252.270214040197,49.2786533779784,0.725,0.774844106654469,284.846451158329,33.25178628121348 +6000.0,0.25,40600.0,2.32024196895106,14.0514983283886,252.270214040197,49.2786533779784,0.7,0.748125344356039,284.846451158329,32.602876148172896 +6000.0,0.25,0.0,0.0,0.0,252.270214040197,49.2786533779784,0.0,0.0,284.846451158329,0.0 +6000.0,0.3,58000.0,0.490355368823204,162.17156541481,253.643108217884,50.2223799446227,1.0,1.06585415420823,341.815741389994,79.52169777161552 +6000.0,0.3,56550.0,0.490230015520132,150.194515838868,253.643108217884,50.2223799446227,0.975,1.03920780035303,341.815741389994,73.62985983072697 +6000.0,0.3,55100.0,0.491270347633714,137.307598486989,253.643108217884,50.2223799446227,0.95,1.01256144649782,341.815741389994,67.45515164145351 +6000.0,0.3,53650.0,0.499436382162378,122.840419870291,253.643108217884,50.2223799446227,0.925,0.985915092642614,341.815741389994,61.35097488332563 +6000.0,0.3,52200.0,0.514875974769656,109.329710607968,253.643108217884,50.2223799446227,0.9,0.959268738787408,341.815741389994,56.291241320561916 +6000.0,0.3,50750.0,0.549133428852494,93.7290481476368,253.643108217884,50.2223799446227,0.875,0.932622384932202,341.815741389994,51.4697535923923 +6000.0,0.3,49300.0,0.606806016429206,77.1385360951671,253.643108217884,50.2223799446227,0.85,0.905976031076996,341.815741389994,46.808127801088865 +6000.0,0.3,47850.0,0.712495760913284,59.1889680071433,253.643108217884,50.2223799446227,0.825,0.879329677221791,341.815741389994,42.17188879792159 +6000.0,0.3,46400.0,0.887450965694075,43.0247513456544,253.643108217884,50.2223799446227,0.8,0.852683323366585,341.815741389994,38.18235713044845 +6000.0,0.3,44950.0,1.12633670920937,31.4866362157395,253.643108217884,50.2223799446227,0.775,0.826036969511379,341.815741389994,35.464554219308596 +6000.0,0.3,43500.0,1.4034241986148,24.134415868178,253.643108217884,50.2223799446227,0.75,0.799390615656173,341.815741389994,33.87082324883402 +6000.0,0.3,42050.0,1.74296021254557,18.8911699676513,253.643108217884,50.2223799446227,0.725,0.772744261800967,341.815741389994,32.926557622052 +6000.0,0.3,40600.0,2.20412661882757,14.6262318459538,253.643108217884,50.2223799446227,0.7,0.746097907945762,341.815741389994,32.23806694481028 +6000.0,0.3,0.0,0.0,0.0,253.643108217884,50.2223799446227,0.0,0.0,341.815741389994,0.0 +6000.0,0.35,58000.0,0.481024129071741,167.733120980767,255.265619518787,51.3542451430885,1.0,1.06246138002961,398.78503162166,80.68367843625842 +6000.0,0.35,56550.0,0.479815230117078,155.374522056274,255.265619518787,51.3542451430885,0.975,1.03589984552887,398.78503162166,74.55106205476213 +6000.0,0.35,55100.0,0.480073596902173,141.88945996365,255.265619518787,51.3542451430885,0.95,1.00933831102813,398.78503162166,68.11738340725633 +6000.0,0.35,53650.0,0.486963945452877,127.295930681106,255.265619518787,51.3542451430885,0.925,0.982776776527389,398.78503162166,61.98852864456732 +6000.0,0.35,52200.0,0.501935998523042,113.225016032718,255.265619518787,51.3542451430885,0.9,0.956215242026649,398.78503162166,56.83171148016976 +6000.0,0.35,50750.0,0.534003617986606,97.1665008385878,255.265619518787,51.3542451430885,0.875,0.929653707525908,398.78503162166,51.88726299490447 +6000.0,0.35,49300.0,0.590090920144534,79.6211174648268,255.265619518787,51.3542451430885,0.85,0.903092173025168,398.78503162166,46.98369846775567 +6000.0,0.35,47850.0,0.691344390788991,60.9981721557301,255.265619518787,51.3542451430885,0.825,0.876530638524428,398.78503162166,42.17074416824522 +6000.0,0.35,46400.0,0.856586290593151,44.4067433965466,255.265619518787,51.3542451430885,0.8,0.849969104023688,398.78503162166,38.038207603369756 +6000.0,0.35,44950.0,1.07852868216672,32.6532715537887,255.265619518787,51.3542451430885,0.775,0.823407569522947,398.78503162166,35.21748993733977 +6000.0,0.35,43500.0,1.33152325107151,25.1885397904928,255.265619518787,51.3542451430885,0.75,0.796846035022207,398.78503162166,33.539126391581064 +6000.0,0.35,42050.0,1.64510575006291,19.7877084172638,255.265619518787,51.3542451430885,0.725,0.770284500521467,398.78503162166,32.552872897808925 +6000.0,0.35,40600.0,2.07601315096264,15.3242670915128,255.265619518787,51.3542451430885,0.7,0.743722966020726,398.78503162166,31.813380010844575 +6000.0,0.35,0.0,0.0,0.0,255.265619518787,51.3542451430885,0.0,0.0,398.78503162166,0.0 diff --git a/Model/data/engine_gpr_model.pth b/Model/data/engine_gpr_model.pth new file mode 100644 index 0000000000000000000000000000000000000000..e9dc524ad3455130ff4dffb5228f7283df25f9ba GIT binary patch literal 6457 zcmbuDYit}>6~|}o*p4?3yG|3Y?KCyb%XXUe&aQXYn>Jl{6BgR-!=2K0S{{@2?$kTZ zc*kevu1yL{(^t|`3ZxV_K`NwzQmHBkBm_up6Q`;gK7dd`q5^?x6}3|Og}?`d5Fa>q z?s|P^cE`^vk7w+e`ThTM=G=SFozYC=20_@kQK)HMLaU$_$F-uWj8B=$q>)$k!F-lv zgHy-#bgD^sh_;grxtyWvYK~||(emGI=2cV8AJuZi4?@2LzCO7Z(YkhA)wP0Q+ zvs5K#OlQrYnVnJWzo8*V3^P{onTCw-6trf1bmxDjKn-pp zuvvmFa>Uyub3A2gh8a|dnJrom8|I{%S9Bu>TMOu`u%WtwSUX+nLA?PNb7SK!Pqg?k!S{Hb%c_;e%*J64Z?u)l|leQZWzR z4w(q_Fqxg4%yk5INpStDWlB?1RheFg%n*Ev$=txn>?W{Bg1z!Ke40r$Tcqh%($(OM zs*M+j1^XO`DC|c>Gl4!t;7e}I_)bGV-O+;q_7)BhxKV?wh zi19F6M3yvR#)=Rak{~MYM3$Y&3;t+`b?h~yz+PO;!Hq+l`5dlrGmWPqfj(`+hZy@N z0yj%=SiaHC=C}$=MtqGqwTLg0c0Ra;X;pefS~s$!+9{HDrx=D27V2BMP^SnSk>IGj zdwD$+QsL8;M`FkXq#5!v9P+aSZj<14yH70@9^8?C3slwsq0g*p^61C?&J~$II{pyK@uX$&F(0WEq}fXEx&* zzK6i6GMr|ocDYUkYx+MtU501a#rxeCy9Oezr@`0D@GQIQ8oa9}ui+bIc+Ngwvc`g= znU;;Y3rGGok25dbMm_6=UZJD5UI@~eGE0=odrPObva-Rh9q2ES3jFFHe|>^Z+H~_4 z*ewL>2vn*8C97-W#Yr3u)2RWMMpdlNL2QT`NY20&nx%B}Hqb4^Y7LB?AgV#Do^&d_ z3f%=Ox<-huQV&lZ{(Dz5>eJ2JL#Gg}^+0>fU?`muS8o8t=0A*eCl~hA$%S9a-=(u8 z-Ij84z{81{PVHZ*6O_Q#K1X$*ZhP_TFNj$m@6f zq5at-?IT^==YD$se?LyXKRUlJZ*3WwX#La4Kfl~Q61~*==B3~K#eV)jit=6SdH!MU z`32$R2z}V+71Ov!OcUu;WFe*teQPCq)VH5$V&di3m)8%}8-eZ7PsHJG zE#ogf?)h`)&e0uss8V} z&4V{JT}@uD?5Y0M*ZTjtE`1ybSE}!$f;}HJy|R4$6A%)$0k0NT(G>>V)hQ!>YT&)^ zkKea^{j*=_uQd>~yPu_+8dhMB*eg**%coO&SLP=akAr6r*u5 zmWYIhA_*}u6px3)kpy*l6OQKj9m|d3E74vDB7`GD z4aQa8x@mk_Ntf4)?Mz)-O)cvN@-<@jA`Oo^aQReYuUebBIedjj7q^RTwjPJomu?7O zt~nvSxH79T!-i=$gs-`rkh7kUup7b`PEJSwmtOUqMBEU*`f)2UTX4|pGBOMGxaEysmmQ*ew3%5H5OY(M;$Nq=bzC`b2I&b xO53i+gX{o*(=S7`E$pwSt8Hw*Ml@}BAAaNzg!e1{*%yf>`>Gvy7rmZc`+vdqW)=Vd literal 0 HcmV?d00001 diff --git a/Model/src/battery_sim.py b/Model/src/battery_sim.py new file mode 100644 index 0000000..cbd4b8d --- /dev/null +++ b/Model/src/battery_sim.py @@ -0,0 +1,168 @@ +import numpy as np +import matplotlib.pyplot as plt + +class BatterySim: + """ + 混电飞机高压电池模型 + """ + def __init__(self, capacity_kwh=50.0, nom_voltage=500.0, initial_soc=0.5): + """ + 初始化电池参数 + :param capacity_kwh: 电池总能量 (kWh),默认 50 kWh + :param nom_voltage: 额定电压 (V),默认 500 V + :param initial_soc: 初始荷电状态 (0.0 到 1.0) + """ + # --- 基本物理参数 --- + # 单体电池为 3.7V,根据额定电压计算串联数 + self.cells_in_series = int(nom_voltage / 3.7) + + # 电池组电压安全极限 (工程极限) + self.V_min = self.cells_in_series * 2.7 # 绝对放电截止电压 (约 364.5V) + self.V_max = self.cells_in_series * 4.2 # 绝对充电截止电压 (约 567.0V) + + # 容量与内阻标定 + self.capacity_Ah = (capacity_kwh * 1000) / nom_voltage # 安时容量 (约 100Ah) + + # 电池内阻为 0.15 欧姆。 + # 在 SOC=50% 时,此内阻可将最大放电/充电安全功率限制在 300kW 左右。 + self.R_in = 0.15 + + # --- 状态变量 --- + self.SOC = initial_soc + self.V_t = 0.0 # 端电压 (V) + self.I = 0.0 # 实际电流 (A),放电为正,充电为负 + + # --- OCV-SOC 查表曲线 Voc = f(SOC) --- + self.soc_table = np.array([0.0, 0.1, 0.2, 0.5, 0.8, 0.9, 1.0]) + self.cell_ocv_table = np.array([2.8, 3.3, 3.4, 3.6, 3.9, 4.0, 4.15]) + self.Voc_table = self.cell_ocv_table * self.cells_in_series + + def _get_ocv(self, soc): + """根据当前 SOC 线性插值获取开路电压""" + return np.interp(soc, self.soc_table, self.Voc_table) + + def step(self, dt, P_req_kw): + """ + 单步执行电池仿真,包含过充/过放保护(功率限幅) + :param dt: 仿真步长 (秒) + :param P_req_kw: 外部请求功率 (kW)。正数表示放电,负数表示充电 + :return: (实际输出/吸收功率 kW, 端电压 V, 实际电流 A, 当前 SOC) + """ + # 1. 获取当前开路电压 + V_oc = self._get_ocv(self.SOC) + + # 2. 计算工程安全极限 (基于电压边界) + # 2.1 最大放电极限 (限制 V_t >= V_min) + I_dis_max = (V_oc - self.V_min) / self.R_in + P_dis_max_W = self.V_min * I_dis_max # 瓦特 + + # 2.2 最大充电极限 (限制 V_t <= V_max, 电流和功率为负数) + I_cha_max = (V_oc - self.V_max) / self.R_in + P_cha_max_W = self.V_max * I_cha_max # 瓦特 + + # SOC 保护限制 (防止 SOC 突破 0% 和 100%) + if self.SOC <= 0.05: + P_dis_max_W = 0.0 # 电量极低,禁止放电 + if self.SOC >= 0.98: + P_cha_max_W = 0.0 # 电量极高,禁止充电 + + # 3. 保护逻辑:功率限幅 (Clipping) + P_req_W = P_req_kw * 1000.0 + # 强制将需求功率限制在安全充放电区间内 + P_actual_W = max(P_cha_max_W, min(P_req_W, P_dis_max_W)) + + # 4. 根据实际功率求解电流 (一元二次方程) + # R * I^2 - Voc * I + P_actual = 0 + discriminant = V_oc**2 - 4 * self.R_in * P_actual_W + + if discriminant < 0: + self.I = V_oc / (2 * self.R_in) + P_actual_W = V_oc**2 / (4 * self.R_in) + else: + self.I = (V_oc - np.sqrt(discriminant)) / (2 * self.R_in) + + # 5. 计算端电压 + self.V_t = V_oc - self.I * self.R_in + + # 6. 安时积分更新 SOC + delta_soc = (self.I * (dt / 3600.0)) / self.capacity_Ah + self.SOC = self.SOC - delta_soc + self.SOC = max(0.0, min(1.0, self.SOC)) + + P_actual_kw = P_actual_W / 1000.0 + return P_actual_kw, self.V_t, self.I, self.SOC + +if __name__ == "__main__": + import matplotlib + matplotlib.use('Agg') + + # ========================================== + # 【测试示例】展示完整的充放电循环与边界保护 + # ========================================== + plt.rcParams['font.family'] = 'serif' + plt.rcParams['font.serif'] = ['DejaVu Serif', 'Times New Roman'] + plt.rcParams['axes.unicode_minus'] = True + + battery = BatterySim(initial_soc=0.5) + + dt = 0.5 + # 仿真总时长 1 小时,包含一个完整的充放电循环,且在边界处持续请求以验证保护机制 + time_array = np.arange(0, 3600, dt) + + req_power_log = [] + act_power_log = [] + soc_log = [] + vt_log = [] + + for t in time_array: + if t < 100: + P_req = 0.0 + elif t < 1200: + # 持续放电 150kW,将SOC从50%消耗至最低限制(5%) + # 在触发限制后继续请求,以验证禁止放电保护生效 + P_req = 150.0 + elif t < 3300: + # 持续充电 150kW,将SOC从5%一直充满至最高限制(98%) + # 同样在触发满电后继续请求,验证禁止充电保护生效 + P_req = -150.0 + else: + P_req = 0.0 + + P_act, V_t, I_act, soc = battery.step(dt, P_req) + + req_power_log.append(P_req) + act_power_log.append(P_act) + vt_log.append(V_t) + soc_log.append(soc * 100) + + # 绘图展示 + fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 8), sharex=True) + + ax1.plot(time_array, req_power_log, 'k--', linewidth=2, label='Requested Power (kW)') + ax1.plot(time_array, act_power_log, 'r-', linewidth=2, label='Actual Power (kW)') + ax1.set_ylabel('Power [kW]') + ax1.set_title('Battery Power: Complete Cycle Protection Demo', fontweight='bold') + ax1.legend() + ax1.grid(True) + + ax2.plot(time_array, vt_log, 'b-', linewidth=2) + ax2.set_ylabel('Terminal Voltage [V]') + ax2.axhline(battery.V_min, color='r', linestyle=':', label='V_min limit') + ax2.axhline(battery.V_max, color='g', linestyle=':', label='V_max limit') + ax2.set_title('Voltage Dynamics with Physical Limits', fontweight='bold') + ax2.legend() + ax2.grid(True) + + ax3.plot(time_array, soc_log, 'g-', linewidth=2) + ax3.axhline(5, color='r', linestyle=':', alpha=0.8, label='SOC Lower Limit (5%)') + ax3.axhline(98, color='g', linestyle=':', alpha=0.8, label='SOC Upper Limit (98%)') + ax3.set_ylabel('SOC [%]') + ax3.set_xlabel('Time [s]') + ax3.set_title('State of Charge (SOC) Evolution', fontweight='bold') + ax3.legend() + ax3.grid(True) + + plt.tight_layout() + plt.savefig('figures/battery_sim_output.png') + print('Plot saved to figures/battery_sim_output.png') + # plt.show() diff --git a/Model/src/engine_dynamic_sim.py b/Model/src/engine_dynamic_sim.py new file mode 100644 index 0000000..d2e52a7 --- /dev/null +++ b/Model/src/engine_dynamic_sim.py @@ -0,0 +1,358 @@ +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import numpy as np +import matplotlib.pyplot as plt +from src.engine_gpr_class import EngineGPRModel +from src.increPID import IncrementalPIDController + +class TurboshaftDynamicSim: + """ + 涡轴发动机动态仿真类 + 包含一阶燃油执行机构和转子动力学模型 + """ + def __init__(self, gpr_csv_path="data/Cleaned_Engine_Data_Full.csv", gpr_pth_path="data/engine_gpr_model.pth", + tau_fuel=0.15, K_inertia=100.0, + kp=4.652, ki=7.078, kd=0.222, min_fuel=10.0, max_fuel=600.0, + verbose=False): + """ + 初始化仿真环境和代理模型 + :param gpr_csv_path: GPR模型使用的数据集路径 + :param gpr_pth_path: 预训练的模型参数路径 + :param tau_fuel: 燃油执行机构时间常数 (s) + :param K_inertia: 转子惯性增益 (RPM / (kg/h)) + :param kp: PID比例系数 (用于功率跟随控制,归一化域) + :param ki: PID积分系数 (归一化域) + :param kd: PID微分系数 (归一化域) + :param min_fuel: 燃油流量下限 (kg/h) + :param max_fuel: 燃油流量上限 (kg/h) + :param verbose: 是否打印详细信息 + """ + import os + self.verbose = verbose + + if verbose: + print("-> 正在加载 GPR 稳态代理模型...") + self.engine_model = EngineGPRModel(gpr_csv_path) + + # 检查模型文件是否存在,不存在则训练 + if not os.path.exists(gpr_pth_path): + if verbose: + print(f"-> 模型文件 {gpr_pth_path} 不存在,正在训练模型...") + self.engine_model.train(save_path=gpr_pth_path) + else: + success = self.engine_model.load_model(gpr_pth_path) + if not success: + if verbose: + print(f"-> 模型加载失败,正在重新训练...") + self.engine_model.train(save_path=gpr_pth_path) + + # 动态参数 + self.tau_fuel = tau_fuel + self.K_inertia = K_inertia + + # 归一化基准 (用于PID计算) + self.max_fuel = max_fuel + self.max_power_ref = 300.0 # 300kw为功率归一化基准,根据实际数据调整 + + # 控制器初始化 (使用归一化参数,通过 scaling 自动处理) + self.pid = IncrementalPIDController( + kp=kp, ki=ki, kd=kd, dt=0.01, + output_min=min_fuel, output_max=max_fuel, + input_scale=self.max_power_ref, output_scale=self.max_fuel + ) + + # 环境与状态变量 + self.H_env = 0.0 + self.Ma_env = 0.0 + self.N_current = 0.0 + self.Wf_act_current = 0.0 + self.power_generated = 0.0 + + # 控制指令 + self.Wf_cmd = 0.0 + + def _solve_steady_rpm(self, target_val, target_type='power'): + """ + 内部方法:反解稳态转速 (RPM) + :param target_val: 目标值 (Power[kW] 或 Fuel[kg/h]) + :param target_type: 'power' 或 'fuel' + :return: 对应的稳态转速 + """ + from scipy.optimize import brentq + + # 搜索范围 [RPM_min, RPM_max],根据经验或数据范围设定 + low_bound, high_bound = 0.0, 60000.0 + + def objective(n): + current_input = np.array([[self.H_env, self.Ma_env, n]]) + pred_mean, _ = self.engine_model.predict(current_input) + + # index 0: Fuel Flow (kg/h), index 1: Power (kW) + val = pred_mean[0, 1] if target_type == 'power' else pred_mean[0, 0] + return val - target_val + + try: + # 简单的边界检查,防止报错 + f_low = objective(low_bound) + f_high = objective(high_bound) + if f_low * f_high > 0: + print(f"[Warn] 目标值超出模型范围 [{low_bound}, {high_bound}] RPM 对应的输出。将使用最接近的边界。") + return low_bound if abs(f_low) < abs(f_high) else high_bound + + n_solution = brentq(objective, low_bound, high_bound) + return n_solution + except Exception as e: + print(f"[Error] 稳态求解失败: {e}") + return (low_bound + high_bound) / 2.0 + + def set_steady_state_by_power(self, H_env, Ma_env, Power_target): + """ + 通过目标功率初始化稳态 + :param Power_target: 目标轴功率 (kW) + :return: 对应的稳态转速 (RPM) + """ + self.H_env = H_env + self.Ma_env = Ma_env + + # 反解转速 + self.N_current = self._solve_steady_rpm(Power_target, target_type='power') + + # 计算该转速下的稳态燃油 + current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) + pred_mean, _ = self.engine_model.predict(current_input) + + self.power_generated = pred_mean[0, 1] + self.Wf_act_current = pred_mean[0, 0] + self.Wf_cmd = self.Wf_act_current + + print(f"-> 稳态(Power)已配置: H={H_env}, Ma={Ma_env}, Target_P={Power_target:.1f} kW => N={self.N_current:.1f} RPM, Wf={self.Wf_cmd:.2f} kg/h") if self.verbose else None + + # 重置PID控制器至当前稳态输出 (自动处理归一化) + self.pid.reset(initial_output=self.Wf_cmd) + + return self.N_current + + def set_steady_state_by_fuel(self, H_env, Ma_env, Wf_target): + """ + 通过目标燃油流量初始化稳态 + :param Wf_target: 目标燃油流量 (kg/h) + :return: 对应的稳态转速 (RPM) + """ + self.H_env = H_env + self.Ma_env = Ma_env + + # 反解转速 + self.N_current = self._solve_steady_rpm(Wf_target, target_type='fuel') + + # 确认该状态下的功率 + current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) + pred_mean, _ = self.engine_model.predict(current_input) + + self.power_generated = pred_mean[0, 1] + self.Wf_act_current = pred_mean[0, 0] # 理论上应该非常接近 Wf_target + self.Wf_cmd = self.Wf_act_current + + print(f"-> 稳态(Fuel)已配置: H={H_env}, Ma={Ma_env}, Target_Wf={Wf_target:.1f} kg/h => N={self.N_current:.1f} RPM, Power={self.power_generated:.1f} kW") if self.verbose else None + + # 重置PID控制器至当前稳态输出 (自动处理归一化) + self.pid.reset(initial_output=self.Wf_cmd) + + return self.N_current + + def set_fuel_command(self, Wf_cmd): + """ + 更改燃油流量指令 + :param Wf_cmd: 目标燃油流量指令 + """ + self.Wf_cmd = Wf_cmd + + def set_flight_condition(self, H_env=None, Ma_env=None): + """ + 在运行过程中更改当前的飞行条件 (高度和马赫数) + :param H_env: 新的飞行高度 (m)。如果为 None,则保持不变。 + :param Ma_env: 新的飞行马赫数。如果为 None,则保持不变。 + """ + if H_env is not None: + self.H_env = H_env + if Ma_env is not None: + self.Ma_env = Ma_env + + def compute_control_law(self, dt, target_power): + """ + 计算控制律 (PID控制: Power -> Wf) + :param dt: 控制周期 (s) + :param target_power: 期望功率 (kW) + :return: 计算出的燃油指令 + """ + self.pid.dt = dt + # 1. 计算控制增量 (PID内部会自动处理归一化) + self.Wf_cmd = self.pid.compute(setpoint=target_power, measurement=self.power_generated) + return self.Wf_cmd + + def step(self, dt, target_power=None): + """ + 执行单步动态仿真 + :param dt: 积分步长 (s) + :param target_power: 目标轴功率 (kW),若不为 None 则执行一次PID控制 + :return: (当前转速, 实际供油量, 当前需要的平衡供油量, 当前功率) + """ + # 0. 闭环控制计算 + if target_power is not None: + self.compute_control_law(dt, target_power) + + # 1. 燃油执行机构动态 (一阶惯性) + dWf_act_dt = (self.Wf_cmd - self.Wf_act_current) / self.tau_fuel + Wf_act_next = self.Wf_act_current + dWf_act_dt * dt + + # 2. 调用GPR代理模型计算当前转速下的阻力矩(需求燃油) + current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) + pred_mean, _ = self.engine_model.predict(current_input) + + # GPR 输出: [0]: Fuel Flow, [1]: Shaft Power + Wf_req_current = pred_mean[0, 0] #维持当前转速所需的稳态燃油 + Power_current = pred_mean[0, 1] + self.power_generated = Power_current # 更新当前功率状态 + + # 3. 转子动力学积分 (燃料差额 -> 转速加速度) + dN_dt = self.K_inertia * (self.Wf_act_current - Wf_req_current) + N_next = self.N_current + dN_dt * dt + + # 4. 状态更新 + self.Wf_act_current = Wf_act_next + self.N_current = N_next + + return self.N_current, self.Wf_act_current, Wf_req_current, Power_current + + +if __name__ == "__main__": + # ========================================== + # 【测试示例】利用类运行功率闭环控制仿真 + # ========================================== + import matplotlib + matplotlib.use('Agg') + import matplotlib.pyplot as plt + + plt.rcParams['font.family'] = 'serif' + plt.rcParams['font.serif'] = ['DejaVu Serif', 'Times New Roman'] + plt.rcParams['axes.unicode_minus'] = True + + # 初始化仿真 (优化后的PID参数) + # kp: 比例系数 - 增大以加快响应速度 + # ki: 积分系数 - 适中以消除稳态误差 + # kd: 微分系数 - 设为0避免控制振荡 + sim = TurboshaftDynamicSim() + + # 1. 设置初始稳态 (通过目标功率设定) + P_initial_target = 100.0 # kW + sim.set_steady_state_by_power(H_env=0.0, Ma_env=0.0, Power_target=P_initial_target) + + # 仿真参数 + dt = 0.02 + t_end = 30.0 # 增加仿真时间以展示更多指令变化 + time_array = np.arange(0, t_end, dt) + + # 数据记录 + N_log = [] + Wf_act_log = [] + Wf_cmd_log = [] + Power_log = [] + Power_target_log = [] + + # 定义多段功率指令 (时间[s], 目标功率[kW]) + power_profile = [ + (0.0, 100.0), # 初始稳态 + (3.0, 200.0), # 阶跃上升 + (8.0, 150.0), # 阶跃下降 + (13.0, 250.0), # 阶跃上升至高功率 + (18.0, 100.0), # 快速下降 + (23.0, 180.0), # 再次上升 + ] + + # 斜坡指令测试:从18kW开始以一定速率上升 + ramp_start_time = 25.0 + ramp_rate = 10.0 # kW/s + + # 设定飞行条件改变的时间 + flight_change_time = 28.0 + + def get_target_power(t, profile, ramp_start, ramp_rate, default_power): + """根据时间获取当前目标功率""" + for i, (time, _) in enumerate(profile): + if t < time: + return profile[i-1][1] if i > 0 else default_power + # 如果在斜坡区间 + if t >= ramp_start: + last_static_power = profile[-1][1] + ramp_power = last_static_power + ramp_rate * (t - ramp_start) + return min(ramp_power, 300.0) # 限制最大300kW + return profile[-1][1] + + print("-> 开始仿真步进 (闭环控制)...") + print("-> 功率指令配置文件:") + for time, power in power_profile: + print(f" t={time:.1f}s -> {power:.0f} kW") + print(f" t={ramp_start_time:.1f}s -> 斜坡上升 (速率 {ramp_rate} kW/s)") + print() + + for t in time_array: + # 确定当前的目标功率 + current_target_P = get_target_power(t, power_profile, ramp_start_time, ramp_rate, P_initial_target) + + # 触发飞行条件变化 (确保仅触发一次以免重复打印) + if flight_change_time <= t < flight_change_time + dt: + print(f"\n[!] 时间 t={t:.2f}s, 模拟飞行条件跃变...") + sim.set_flight_condition(H_env=3000.0, Ma_env=0.2) + + # 执行闭环仿真步进 + N_cur, Wf_act_cur, Wf_req, Power_cur = sim.step(dt, target_power=current_target_P) + + # 记录数据 + N_log.append(N_cur) + Wf_act_log.append(Wf_act_cur) + Wf_cmd_log.append(sim.Wf_cmd) + Power_log.append(Power_cur) + Power_target_log.append(current_target_P) + + # 绘图 + fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10)) + fig.suptitle(f'Turboshaft Engine Dynamic Simulation\nPID Parameters: Kp={sim.pid.kp}, Ki={sim.pid.ki}, Kd={sim.pid.kd}', + fontsize=12, fontweight='bold') + + # Plot 1: Power Tracking + ax1.plot(time_array, Power_target_log, 'k--', linewidth=2, label='Target Power') + ax1.plot(time_array, Power_log, 'g-', linewidth=2, label='Actual Power') + ax1.set_ylabel('Shaft Power [kW]') + ax1.set_title('Closed-Loop Control: Power Tracking', fontweight='bold') + ax1.grid(True, linestyle=':', alpha=0.7) + ax1.legend() + + # Plot 2: Rotor Speed + ax2.plot(time_array, N_log, 'b-', linewidth=2, label='Engine Speed (N)') + ax2.set_ylabel('Rotor Speed [RPM]') + ax2.set_title('Engine Response: Rotor Speed', fontweight='bold') + ax2.grid(True, linestyle=':', alpha=0.7) + ax2.legend() + + # Plot 3: Fuel Flow (Control Input) + ax3.plot(time_array, Wf_cmd_log, 'k--', linewidth=1.5, label='Fuel Command') + ax3.plot(time_array, Wf_act_log, 'r-', linewidth=2, label='Actual Fuel') + ax3.set_xlabel('Time [s]') + ax3.set_ylabel('Fuel Flow [kg/h]') + ax3.set_title('Control Effort: Fuel Flow', fontweight='bold') + ax3.grid(True, linestyle=':', alpha=0.7) + ax3.legend() + + plt.tight_layout(rect=[0, 0, 1, 0.96]) + plt.savefig('figures/engine_dynamic_sim_plot.png') + print('Plot saved to figures/engine_dynamic_sim_plot.png') + + # 保存数据到 .dat 文件 + dat_file = 'data/pid_tuning_data.dat' + with open(dat_file, 'w') as f: + f.write('# Time[s]\tTarget_Power[kW]\tActual_Power[kW]\tRotor_Speed[RPM]\tFuel_Command[kg/h]\tActual_Fuel[kg/h]\n') + for i in range(len(time_array)): + f.write(f'{time_array[i]:.4f}\t{Power_target_log[i]:.2f}\t{Power_log[i]:.2f}\t{N_log[i]:.2f}\t{Wf_cmd_log[i]:.2f}\t{Wf_act_log[i]:.2f}\n') + print(f'Data saved to {dat_file}') + # plt.show() diff --git a/Model/src/engine_gpr_class.py b/Model/src/engine_gpr_class.py new file mode 100644 index 0000000..ab18054 --- /dev/null +++ b/Model/src/engine_gpr_class.py @@ -0,0 +1,283 @@ +import torch +import pandas as pd +import numpy as np +import matplotlib.pyplot as plt +from matplotlib.colors import LogNorm +from botorch.models import SingleTaskGP +from botorch.fit import fit_gpytorch_mll +from gpytorch.mlls import ExactMarginalLogLikelihood +from gpytorch.means import LinearMean +from gpytorch.priors import GammaPrior +from sklearn.preprocessing import StandardScaler +import warnings +import os + +warnings.filterwarnings("ignore") + +# ========================================== +# 【全局绘图设置】学术规范 +# ========================================== +plt.rcParams['font.family'] = 'serif' +plt.rcParams['font.serif'] = ['DejaVu Serif', 'Times New Roman'] +plt.rcParams['mathtext.fontset'] = 'stix' +plt.rcParams['axes.unicode_minus'] = True + +class EngineGPRModel: + """ + 涡轴发动机 GPR 代理模型类 + 支持: 训练、加载已有模型、预测推断、单马赫数切面可视化 + """ + def __init__(self, csv_path="data/Cleaned_Engine_Data_Full.csv"): + self.csv_path = csv_path + self.df = None + self.valid_X_cols = [] + self.scaler_X = StandardScaler() + self.scaler_Y = StandardScaler() + self.model = None + + def _prepare_data(self): + """数据预处理:读取、筛选、Log变换、标准化""" + self.df = pd.read_csv(self.csv_path) + X_df = self.df[['Altitude_m', 'Mach', 'RPM']] + + # 剔除无效特征 + self.valid_X_cols = [col for col in X_df.columns if X_df[col].nunique() > 1] + X_numpy = self.df[self.valid_X_cols].values + + if 'WF_kg_h' not in self.df.columns: + raise ValueError("Column 'WF_kg_h' not found.") + + Y_numpy = self.df[['WF_kg_h', 'Power_kW']].values + + # Log1p 变换:保证非负性,并线性化指数规律 + Y_numpy = np.log1p(Y_numpy) + + # Z-Score 归一化 + X_scaled = self.scaler_X.fit_transform(X_numpy) + Y_scaled = self.scaler_Y.fit_transform(Y_numpy) + + return torch.tensor(X_scaled, dtype=torch.double), torch.tensor(Y_scaled, dtype=torch.double) + + def _init_model(self, train_X, train_Y): + """内部方法:统一初始化模型结构(包含 Mean 和 Prior 设置)""" + mean_module = LinearMean(input_size=train_X.shape[-1], batch_shape=torch.Size([train_Y.shape[-1]])) + model = SingleTaskGP(train_X, train_Y, mean_module=mean_module) + + # 统一施加 GammaPrior 防止结构不匹配 + # 这一步非常关键:加载模型时,模型结构必须与训练时完全一致,包括 Prior + if hasattr(model.covar_module, 'base_kernel'): + kernel = model.covar_module.base_kernel + else: + kernel = model.covar_module + + if hasattr(kernel, 'lengthscale'): + kernel.lengthscale_prior = GammaPrior(4.0, 1.0) + + return model + + def train(self, save_path=None): + """训练 GPR 模型 (Log-Space + 平滑约束)""" + print(f"{'='*30}\n🚀 GP Training (Log-Space)\n{'='*30}") + + train_X, train_Y = self._prepare_data() + + # 使用统一初始化方法 + self.model = self._init_model(train_X, train_Y) + + # 仅在训练开始前设定初始值,引导优化方向 + if hasattr(self.model.covar_module, 'base_kernel'): + kernel = self.model.covar_module.base_kernel + else: + kernel = self.model.covar_module + + if hasattr(kernel, 'lengthscale'): + kernel.lengthscale = 2.0 + + print("-> Optimizing hyperparameters...") + mll = ExactMarginalLogLikelihood(self.model.likelihood, self.model) + fit_gpytorch_mll(mll) + print("-> Training completed.") + + if save_path: + torch.save(self.model.state_dict(), save_path) + print(f"-> Model saved to {save_path}") + + def load_model(self, pth_path="data/engine_gpr_model.pth"): + """加载已训练的模型""" + print(f"Loading model from {pth_path}...") + try: + train_X, train_Y = self._prepare_data() + # 必须使用完全相同的结构初始化,否则 load_state_dict 会报错 + self.model = self._init_model(train_X, train_Y) + + # 使用 strict=False 忽略 Prior 缓冲区的差异(例如 _transformed_loc 等内部参数) + # 这些参数通常不影响模型预测,只影响后续继续训练时的约束 + self.model.load_state_dict(torch.load(pth_path), strict=True) + self.model.eval() + print("-> Model loaded successfully.") + return True + except Exception as e: + print(f"-> Load failed: {e}") + return False + + def _predict_log_space(self, test_X_real: np.ndarray): + """预测 Log 空间下的均值和标准差""" + if self.model is None: raise ValueError("Model not initialized.") + + self.model.eval() + test_X_scaled = torch.tensor(self.scaler_X.transform(test_X_real), dtype=torch.double) + + with torch.no_grad(): + posterior = self.model.posterior(test_X_scaled) + mu_scaled = posterior.mean.numpy() + var_scaled = posterior.variance.numpy() + + # 反归一化 + mu_log_real = self.scaler_Y.inverse_transform(mu_scaled) + var_log_real = var_scaled * self.scaler_Y.var_ + return mu_log_real, np.sqrt(var_log_real) + + def predict(self, test_X_real: np.ndarray): + """预测物理值 (expm1 还原)""" + mu_log, std_log = self._predict_log_space(test_X_real) + + # 中位数点估计 (Median of LogNormal) + pred_mean = np.expm1(mu_log) + # 近似方差 + var_log = std_log ** 2 + pred_var = (np.expm1(var_log)) * np.exp(2*mu_log + var_log) + + return pred_mean, pred_var + + def visualize(self, target_mach=0.0): + """可视化:生成方差热力图、切面曲线、均值云图""" + if self.model is None: raise ValueError("Model not initialized.") + print(f"Generating Plots for Mach = {target_mach}") + + # 确定绘图范围 (训练数据 Range) + H_min, H_max = self.df['Altitude_m'].min(), self.df['Altitude_m'].max() + RPM_min, RPM_max = self.df['RPM'].min(), self.df['RPM'].max() + + if H_min == H_max: H_min, H_max = H_min-1, H_max+1 + if RPM_min == RPM_max: RPM_min, RPM_max = RPM_min-100, RPM_max+100 + + H_grid, RPM_grid = np.meshgrid(np.linspace(H_min, H_max, 60), np.linspace(RPM_min, RPM_max, 60), indexing='ij') + + test_X_dict = {'Altitude_m': H_grid.flatten(), 'RPM': RPM_grid.flatten()} + if 'Mach' in self.valid_X_cols: + test_X_dict['Mach'] = np.full_like(H_grid.flatten(), target_mach) + + test_X_real = np.column_stack([test_X_dict[col] for col in self.valid_X_cols]) + + # 预测并重塑 (60x60) + pred_mean, pred_var = self.predict(test_X_real) + wf_mean, pow_mean = pred_mean[:, 0].reshape(60, 60), pred_mean[:, 1].reshape(60, 60) + wf_var, pow_var = pred_var[:, 0].reshape(60, 60), pred_var[:, 1].reshape(60, 60) + + # 筛选绘图用的真实数据点 + tol = 1e-3 + plot_df = self.df[(self.df['Mach'] >= target_mach - tol) & (self.df['Mach'] <= target_mach + tol)] if 'Mach' in self.valid_X_cols else self.df + + # 1. 方差热力图 (Log Scale) + fig, axes = plt.subplots(1, 2, figsize=(14, 5.5)) + for ax, var_data, title in zip(axes, [wf_var, pow_var], ['Fuel Flow Variance', 'Shaft Power Variance']): + log_var = np.log10(np.maximum(var_data, 1e-16)) + cf = ax.contourf(RPM_grid, H_grid, log_var, levels=50, cmap='jet', alpha=0.9) + if not plot_df.empty: ax.scatter(plot_df['RPM'], plot_df['Altitude_m'], c='white', edgecolors='black', s=25, label='Data') + ax.set_title(f"{title} (Log10)"); ax.set_xlabel('RPM'); ax.set_ylabel('Altitude') + plt.colorbar(cf, ax=ax) + plt.tight_layout() + plt.savefig(f'figures/gpr_mach_{target_mach}_variance.png') + print(f"Saved figures/gpr_mach_{target_mach}_variance.png") + plt.close() + + # 2. 高度切面图 (95% CI) + fig, axes = plt.subplots(1, 2, figsize=(14, 5.5)) + colors = ['#1f77b4', '#ff7f0e', '#2ca02c'] + RPM_1D = np.linspace(RPM_min, RPM_max, 100) + + for idx, alt in enumerate([0.0, 3000.0, 6000.0]): + d_1D = {'Altitude_m': np.full_like(RPM_1D, alt), 'RPM': RPM_1D} + if 'Mach' in self.valid_X_cols: d_1D['Mach'] = np.full_like(RPM_1D, target_mach) + X_1D = np.column_stack([d_1D[c] for c in self.valid_X_cols]) + + # Log空间预测 -> 计算物理置信区间 (保证下界非负) + mu_log, std_log = self._predict_log_space(X_1D) + mean = np.expm1(mu_log) + lower = np.expm1(mu_log - 2*std_log) + upper = np.expm1(mu_log + 2*std_log) + + for ax, i in zip(axes, [0, 1]): + ax.plot(RPM_1D, mean[:, i], color=colors[idx], label=f'Alt={int(alt)}m') + ax.fill_between(RPM_1D, lower[:, i], upper[:, i], color=colors[idx], alpha=0.2) + + for ax, title, ylab in zip(axes, ['Fuel Flow', 'Shaft Power'], ['kg/h', 'kW']): + ax.set_title(title); ax.set_ylabel(ylab); ax.set_xlabel('RPM') + ax.legend(); ax.grid(True, alpha=0.5) + plt.tight_layout() + plt.savefig(f'figures/gpr_mach_{target_mach}_section.png') + print(f"Saved figures/gpr_mach_{target_mach}_section.png") + plt.close() + + # 3. 均值云图 + fig, axes = plt.subplots(1, 2, figsize=(14, 5.5)) + for ax, mean_data, title in zip(axes, [wf_mean, pow_mean], ['Fuel Flow Mean', 'Shaft Power Mean']): + cf = ax.contourf(RPM_grid, H_grid, mean_data, levels=50, cmap='viridis', alpha=0.9) + if not plot_df.empty: ax.scatter(plot_df['RPM'], plot_df['Altitude_m'], c='red', s=20, edgecolors='white', label='Data') + ax.set_title(title); ax.set_xlabel('RPM'); ax.set_ylabel('Altitude') + plt.colorbar(cf, ax=ax) + plt.tight_layout() + plt.savefig(f'figures/gpr_mach_{target_mach}_mean.png') + print(f"Saved figures/gpr_mach_{target_mach}_mean.png") + plt.close() + +if __name__ == "__main__": + import matplotlib + matplotlib.use('Agg') + print("Using Agg backend for plotting.") + + engine_model = EngineGPRModel() + + # 智能选择加载或训练 + model_path = "data/engine_gpr_model.pth" + if os.path.exists(model_path): + success = engine_model.load_model(model_path) + if not success: + # 如果加载失败(结构不匹配),则重新训练 + engine_model.train(save_path=model_path) + else: + # 如果模型不存在,则训练并保存 + engine_model.train(save_path=model_path) + + # ========================================== + # 【使用示例】 + # ========================================== + print("\n" + "="*50) + print("💡 预测使用示例 (Prediction Example)") + print("="*50) + # 假设默认特征组合为: [Altitude_m, Mach, RPM] + sample_inputs = np.array([ + [0.0, 0.0, 15000.0], # 测试点 1: 海平面静止, 15000 RPM + [3000.0, 0.2, 22000.0] # 测试点 2: 3000米, 0.2马赫, 22000 RPM + ]) + + + mean_preds, var_preds = engine_model.predict(sample_inputs) + + for i, (X_in, Y_mean, Y_var) in enumerate(zip(sample_inputs, mean_preds, var_preds)): + # 格式化输出 + x_str = "[" + ", ".join([f"{val:8.2f}" for val in X_in]) + " ]" + mean_str = "[" + ", ".join([f"{val:10.4f}" for val in Y_mean]) + " ]" + var_str = "[" + ", ".join([f"{val:10.4f}" for val in Y_var]) + " ]" + + print(f"[测试点 {i+1}]") + print(f" -> 输入特征 {engine_model.valid_X_cols}: {x_str}") + print(f" -> 预测均值 [WF (kg/h), Power_kW]: {mean_str}") + print(f" -> 预测方差 [WF (kg/h), Power_kW]: {var_str}\n") + + # ========================================== + # 循环调用绘图方法,分别输出所需的马赫数切面图 + # ========================================== + target_machs = [0.0, 0.1, 0.2, 0.3, 0.4] + for m in target_machs: + engine_model.visualize(target_mach=m) diff --git a/Model/src/increPID.py b/Model/src/increPID.py new file mode 100644 index 0000000..a58f5ee --- /dev/null +++ b/Model/src/increPID.py @@ -0,0 +1,88 @@ +class IncrementalPIDController: + """ + 增量式 PID 控制器 (支持自动归一化缩放) + 适用于发动机燃油控制等具有保持特性的执行机构 + """ + def __init__(self, kp, ki, kd, dt, output_min, output_max, input_scale=1.0, output_scale=1.0): + """ + 初始化 PID 控制器 + + 如果提供了 scale 参数,则 kp/ki/kd 被视为归一化域的参数。 + 内部运算逻辑: + norm_error = (setpoint - measurement) / input_scale + norm_output += PID(norm_error, kp, ki, kd) + physical_output = norm_output * output_scale + + :param kp: 比例系数 (建议使用归一化参数) + :param ki: 积分系数 + :param kd: 微分系数 + :param dt: 控制周期 (s) + :param output_min: 执行机构输出下限 (物理量) + :param output_max: 执行机构输出上限 (物理量) + :param input_scale: 输入测量值的量程基准 (例如额定功率 1000.0) + :param output_scale: 输出控制量的量程基准 (例如最大燃油 600.0) + """ + self.kp = kp + self.ki = ki + self.kd = kd + self.dt = dt + + # 记录缩放因子 + self.input_scale = input_scale + self.output_scale = output_scale + + # 将物理限制转换为内部的归一化限制 + self.out_min_norm = output_min / output_scale + self.out_max_norm = output_max / output_scale + + # 历史误差状态 (归一化误差) + self.e_k1 = 0.0 # e(k-1) + self.e_k2 = 0.0 # e(k-2) + + # 当前实际的控制输出 (归一化值 [0~1] 或 [-1~1]) + self.current_output_norm = 0.0 + + def reset(self, initial_output=0.0): + """ + 重置控制器状态,用于初始化或开闭环的无扰切换 + :param initial_output: 当前执行机构的实际位置(物理量,如燃油流量) + """ + self.e_k1 = 0.0 + self.e_k2 = 0.0 + # 将物理初始值转换为归一化内部状态 + self.current_output_norm = initial_output / self.output_scale + + def compute(self, setpoint, measurement): + """ + 计算下一拍的控制量 + :param setpoint: 目标设定值 (物理量) + :param measurement: 当前测量值 (物理量) + :return: 经过限幅的绝对控制指令 (物理量) + """ + # 1. 计算归一化误差 e(k) + # 误差除以输入量程,使得误差在 -1~1 之间 (对于阶跃通常更小) + e_k = (setpoint - measurement) / self.input_scale + + # 2. 计算归一化控制增量 delta_u + p_term = self.kp * (e_k - self.e_k1) + i_term = self.ki * e_k * self.dt + # 微分项除以dt可能会很大,归一化的时间常数有助于平滑 + d_term = self.kd * (e_k - 2 * self.e_k1 + self.e_k2) / self.dt + + delta_u_norm = p_term + i_term + d_term + + # 3. 更新当前归一化输出量 + self.current_output_norm += delta_u_norm + + # 4. 绝对位置限幅 (在归一化域进行) + if self.current_output_norm > self.out_max_norm: + self.current_output_norm = self.out_max_norm + elif self.current_output_norm < self.out_min_norm: + self.current_output_norm = self.out_min_norm + + # 5. 更新历史误差 + self.e_k2 = self.e_k1 + self.e_k1 = e_k + + # 6. 返回物理量输出 + return self.current_output_norm * self.output_scale diff --git a/Model/src/motor_sim.py b/Model/src/motor_sim.py new file mode 100644 index 0000000..122716e --- /dev/null +++ b/Model/src/motor_sim.py @@ -0,0 +1,1225 @@ +# -*- coding: utf-8 -*- +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +""" +混合动力涡轴发动机驱动电机动力学模型 (Motor Dynamics Model) + +模块提供了永磁同步电机 (PMSM) 的离散时间仿真实现,涵盖了闭环转速控制、 +物理级的电磁转矩估算及端电压/损耗模型的动力学计算。 + +系统符号学约定: +- 轴系转矩 (T_motor): 正值表示吸收轴系功率 (发电机/负载响应),负值表示向轴系输出功率 (驱动动力)。 +- 直流母线功率 (P_bus): 正值表示从母线汲取有功功率,负值表示向母线回馈有功功率。 +""" + +from typing import Any, Dict +import sys +import os +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import numpy as np +from src.increPID import IncrementalPIDController + + +class MotorSim: + """混电飞机驱动电机模型""" + + def __init__( + self, + n_p: int = 3, + R_s: float = 0.05, + L_d: float = 0.001, + L_q: float = 0.001, + psi_f: float = 0.15, + J: float = 0.5, + u_dc: float = 520.0, + P_rate: float = 300e3, + w_rate: float = 575.95, + eta_mot: float = 0.95, + eta_gen: float = 0.93, + B_visc: float = 3e-4, + k_p_w: float = 13.440362, + k_i_w: float = 42.997816, + k_d_w: float = 0.484660, + k_d_w_error: float = 0.05, + p_bus_slew_rate_kw: float = 2000.0, + t_cmd_slew_rate_nm_s: float = 0.0, + tau_i: float = 0.004, + k_mod: float = 1 / np.sqrt(3), + P_const_loss: float = 250.0, + k_fe: float = 3e-4, + k_inv: float = 0.015, + tau_n_ref: float = 0.35, + tau_t_cmd: float = 0.12, + speed_priority_band_rpm: float = 180.0, + tau_v_bus: float = 0.08, + i_s_max: float | None = None, + ): + """ + 初始化电机动力学仿真器参数。 + + :param n_p: 极对数 + :param R_s: 定子等效电阻 (Ohm) + :param L_d: 直轴电感 (H) + :param L_q: 交轴电感 (H) + :param psi_f: 永磁体磁链 (Wb) + :param J: 轴系总等效转动惯量 (kg·m^2) + :param u_dc: 初始化母线侧额定直流电压 (V) + :param P_rate: 额定机械功率 (W) + :param w_rate: 额定机械角速度 (rad/s) + :param eta_mot: 稳态驱动效率参考值 + :param eta_gen: 稳态发电效率参考值 + :param B_visc: 轴系黏性摩擦阻尼系数 (N·m·s) + :param k_p_w: 速度控制环比例增益 + :param k_i_w: 速度控制环积分增益 + :param k_d_w: 速度控制环微分控制增益(闭环阻尼) + :param k_d_w_error: 目标速度微分前馈增益 + :param p_bus_slew_rate_kw: 允许的最大母线请求功率变化率 (kW/s) + :param t_cmd_slew_rate_nm_s: 允许的最大转矩指令变化率 (Nm/s) + :param tau_i: 闭环电流/电磁转矩等效一阶延迟时间常数 (s) + :param k_mod: DC-AC 变换器电压利用系数 + :param P_const_loss: 独立于工况的系统常量损耗 (W) + :param k_fe: 铁耗计算映射系数 + :param k_inv: DC-AC 逆变环节损耗系数 + :param tau_n_ref: 目标转速给定指令一阶低通滤波时间常数 (s) + :param tau_t_cmd: 转矩输出指令一阶低通滤波时间常数 (s) + :param speed_priority_band_rpm: 目标跟随允许容差基准带 (RPM) + :param tau_v_bus: DC侧动态响应一阶滤波时间常数 (s) + :param i_s_max: 定子相电流约束阈值有效值 (A) + """ + self._validate_parameters( + n_p, + R_s, + L_d, + L_q, + psi_f, + J, + u_dc, + P_rate, + w_rate, + eta_mot, + eta_gen, + tau_i, + ) + + self.n_p = n_p + self.R_s = R_s + self.L_d = L_d + self.L_q = L_q + self.psi_f = psi_f + self.J = J + self.u_dc = u_dc + self.P_rate = P_rate + self.w_rate = w_rate + self.eta_mot = eta_mot + self.eta_gen = eta_gen + self.B_visc = B_visc + self.k_p_w = k_p_w + self.k_i_w = k_i_w + self.k_d_w = k_d_w + self.k_d_w_error = k_d_w_error + self.p_bus_slew_rate_kw = p_bus_slew_rate_kw + self.t_cmd_slew_rate_nm_s = max(0.0, float(t_cmd_slew_rate_nm_s)) + self.tau_i = tau_i + self.k_mod = k_mod + self.P_const_loss = P_const_loss + self.k_fe = k_fe + self.k_inv = k_inv + self.tau_n_ref = max(1e-4, float(tau_n_ref)) + self.tau_t_cmd = max(1e-4, float(tau_t_cmd)) + self.speed_priority_band = max(5.0, float(speed_priority_band_rpm)) * 2.0 * np.pi / 60.0 + self.tau_v_bus = max(1e-4, float(tau_v_bus)) + + self.tau_rate = self.P_rate / self.w_rate + if i_s_max is None: + base_current = self.tau_rate / (1.5 * self.n_p * max(abs(self.psi_f), 1e-6)) + self.i_s_max = max(20.0, 1.8 * base_current) + else: + self.i_s_max = float(i_s_max) + + self.u_s_max = self.u_dc * self.k_mod + + + self.w_M = 0.0 + self.t_motor = 0.0 + self.p_bus = 0.0 + self.p_shaft = 0.0 + self.p_loss = 0.0 + self.i_bus = 0.0 + self.v_motor = self.u_dc + self.i_d = 0.0 + self.i_q = 0.0 + self.i_s = 0j + self.u_d = 0.0 + self.u_q = 0.0 + self.u_s = 0.0 + self.theta_e = 0.0 + self.i_a, self.i_b, self.i_c = 0.0, 0.0, 0.0 + self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0 + self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5 + self._sim_t = 0.0 + self._w_error_int = 0.0 + self._w_error_prev = 0.0 + self._dw_error_f = 0.0 + self._p_bus_req_prev = 0.0 + self._w_set_f = 0.0 + self._t_ref_f = 0.0 + self._v_bus_f = float(self.u_dc) + self._t_load_f = 0.0 + self._t_ext_f = 0.0 + + # 系统机械约束约束配置 + self._dw_max = 800.0 + + # 增量式PID控制器 (用于转速环功率控制) + # output_scale: 额定功率300kW,input_scale: 额定转速575.95 rad/s + self.speed_pid = IncrementalPIDController( + kp=k_p_w, ki=k_i_w, kd=k_d_w, + dt=0.02, # 将在step中动态更新 + output_min=-self.P_rate/1000.0 * 1.2, # 允许放电 + output_max=self.P_rate/1000.0 * 1.2, # 允许充电 + input_scale=self.w_rate, + output_scale=self.P_rate/1000.0 + ) + + self._log = { + 't': [], + 'w_M': [], + 'n_rpm': [], + 't_motor': [], + 't_load': [], + 't_ext': [], + 'p_bus_req_kw': [], + 'p_bus_kw': [], + 'p_shaft_kw': [], + 'p_loss_kw': [], + 'i_bus_a': [], + 'v_bus': [], + 'v_motor': [], + 'mode': [], + 'i_a': [], + 'i_b': [], + 'i_c': [], + 'u_an': [], + 'u_bn': [], + 'u_cn': [], + } + + # 滤波系数缓存 (用于优化: 避免每步重复计算 exp(-dt/tau)) + self._last_dt: float | None = None + self._a_v: float = 0.0 # 母线电压滤波系数 + self._a_n: float = 0.0 # 转速参考滤波系数 + self._a_t_load: float = 0.0 # 负载转矩滤波系数 + self._a_t: float = 0.0 # 转矩指令滤波系数 + self._alpha_i: float = 0.0 # 电流环滤波系数 + + # Magic Numbers 提取为常量 (提高可读性和可维护性) + self._W_MIN_RAD: float = 50.0 # 最小转速阈值 (rad/s),用于避免除零和数值不稳定 + self._EPSILON: float = 1e-6 # 数值 epsilon,用于避免除零 + self._TAU_LOAD_FILTER: float = 0.05 # 负载转矩滤波时间常数 (s) + self._TORQUE_OVERRATE: float = 1.35 # 转矩过载系数 + self._W_ERROR_DEADBAND: float = 0.05 # 转速误差死区 (rad/s) + self._DW_ERROR_FILTER: float = 0.15 # 微分误差滤波系数 + + # 日志记录配置:可选的最大日志长度限制 (None 表示无限制) + self._log_maxlen: int | None = None # 可设置为如 100000 来限制内存使用 + + def _update_filter_coefficients(self, dt: float) -> None: + """更新滤波系数缓存 (当 dt 变化时调用)""" + if self._last_dt == dt: + return + self._last_dt = dt + self._a_v = 1.0 - np.exp(-dt / self.tau_v_bus) + self._a_n = 1.0 - np.exp(-dt / self.tau_n_ref) + self._a_t_load = 1.0 - np.exp(-dt / self._TAU_LOAD_FILTER) + self._a_t = 1.0 - np.exp(-dt / self.tau_t_cmd) + self._alpha_i = 1.0 - np.exp(-dt / self.tau_i) + + def _validate_parameters( + self, + n_p: int, + R_s: float, + L_d: float, + L_q: float, + psi_f: float, + J: float, + u_dc: float, + P_rate: float, + w_rate: float, + eta_mot: float, + eta_gen: float, + tau_i: float, + ) -> None: + """校验主要参数是否满足物理与数值稳定边界。""" + if n_p <= 0: + raise ValueError('n_p must be positive') + if R_s <= 0 or L_d <= 0 or L_q <= 0: + raise ValueError('R_s, L_d and L_q must be positive') + if psi_f <= 0: + raise ValueError('psi_f must be positive') + if J <= 0: + raise ValueError('J must be positive') + if u_dc <= 0 or P_rate <= 0 or w_rate <= 0: + raise ValueError('u_dc, P_rate and w_rate must be positive') + if not (0.0 < eta_mot <= 1.0) or not (0.0 < eta_gen <= 1.0): + raise ValueError('eta_mot and eta_gen must be in (0, 1]') + if tau_i <= 0: + raise ValueError('tau_i must be positive') + + @staticmethod + def clarke_transform(x_a: float, x_b: float, x_c: float) -> tuple[float, float]: + """等幅值 Clarke 变换 (abc -> alpha beta)""" + x_alpha = (2.0/3.0) * (x_a - 0.5 * x_b - 0.5 * x_c) + x_beta = (2.0/3.0) * ((np.sqrt(3.0) / 2.0) * x_b - (np.sqrt(3.0) / 2.0) * x_c) + return float(x_alpha), float(x_beta) + + @staticmethod + def inv_clarke_transform(x_alpha: float, x_beta: float) -> tuple[float, float, float]: + """等幅值逆 Clarke 变换 (alpha beta -> abc)""" + x_a = x_alpha + x_b = -0.5 * x_alpha + (np.sqrt(3.0) / 2.0) * x_beta + x_c = -0.5 * x_alpha - (np.sqrt(3.0) / 2.0) * x_beta + return float(x_a), float(x_b), float(x_c) + + @staticmethod + def park_transform(x_alpha: float, x_beta: float, theta: float) -> tuple[float, float]: + """Park 变换 (alpha beta -> dq)""" + cos_t = np.cos(theta) + sin_t = np.sin(theta) + x_d = x_alpha * cos_t + x_beta * sin_t + x_q = -x_alpha * sin_t + x_beta * cos_t + return float(x_d), float(x_q) + + @staticmethod + def inv_park_transform(x_d: float, x_q: float, theta: float) -> tuple[float, float]: + """逆 Park 变换 (dq -> alpha beta)""" + cos_t = np.cos(theta) + sin_t = np.sin(theta) + x_alpha = x_d * cos_t - x_q * sin_t + x_beta = x_d * sin_t + x_q * cos_t + return float(x_alpha), float(x_beta) + + @staticmethod + def svpwm(u_a: float, u_b: float, u_c: float, u_dc: float) -> tuple[float, float, float, float, float, float]: + """ + 基于共模电压注入的连续 SVPWM 调制 (Min-Max 注入法) + 返回:占空比 d_a, d_b, d_c (0~1) 和实际向电机施加的相电压 u_an, u_bn, u_cn + """ + v_max = max(u_a, u_b, u_c) + v_min = min(u_a, u_b, u_c) + u_zero = -0.5 * (v_max + v_min) # 生成马鞍波零序偏置 + + # 调制后的相电压(相对于参考中点) + u_an = u_a + u_zero + u_bn = u_b + u_zero + u_cn = u_c + u_zero + + # 计算占空比:在 +/- u_dc/2 之间波动映射到 0~1 的占空比 + # 设全桥中点为基准电压 0V,则半桥输出可达 +/- u_dc/2 + d_a = float(np.clip(0.5 + u_an / max(u_dc, 1e-6), 0.0, 1.0)) + d_b = float(np.clip(0.5 + u_bn / max(u_dc, 1e-6), 0.0, 1.0)) + d_c = float(np.clip(0.5 + u_cn / max(u_dc, 1e-6), 0.0, 1.0)) + + return d_a, d_b, d_c, float(u_an), float(u_bn), float(u_cn) + + def _torque_from_currents(self, i_d: float, i_q: float) -> float: + """ + 基于 PMSM 电磁转矩方程计算电机转矩。 + + 注意:正转矩代表吸收轴功率,负转矩代表向轴输出功率。 + + :param i_d: d轴电流 (A) + :param i_q: q轴电流 (A) + :return: 电磁转矩 (Nm) + """ + return 1.5 * self.n_p * (self.psi_f + (self.L_d - self.L_q) * i_d) * i_q + + def _compute_voltage(self, i_d: float, i_q: float, w_M: float, v_bus: float) -> tuple[float, float, float]: + """ + 按 dq 轴稳态方程估算定子电压,并考虑母线电压约束。 + + :param i_d: d轴电流 (A) + :param i_q: q轴电流 (A) + :param w_M: 机械角速度 (rad/s) + :param v_bus: 当前母线电压 (V) + :return: `(u_d, u_q, |u_s|)`,单位均为 V + """ + w_e = self.n_p * abs(w_M) + u_d = self.R_s * i_d - w_e * self.L_q * i_q + u_q = self.R_s * i_q + w_e * (self.L_d * i_d + self.psi_f) + u_s = float(np.hypot(u_d, u_q)) + return u_d, u_q, min(u_s, v_bus * self.k_mod) + + def _max_iq_given_id(self, i_d: float, w_e: float, v_bus: float) -> float: + """计算给定 `i_d` 下受电流圆和电压椭圆约束的 `|i_q|` 上限。""" + if abs(i_d) > self.i_s_max: + return 0.0 + + iq_i = np.sqrt(max(0.0, self.i_s_max**2 - i_d**2)) + if w_e < 1e-6: + return iq_i + + psi_v_max = (v_bus * self.k_mod) / w_e + flux_term = psi_v_max**2 - (self.psi_f + self.L_d * i_d) ** 2 + if flux_term <= 0: + return 0.0 + + iq_v = np.sqrt(flux_term) / max(abs(self.L_q), 1e-9) + return max(0.0, min(iq_i, iq_v)) + + def _compute_tau_limit(self, w_M: float, v_bus: float) -> float: + """ + 计算当前工况下电机可实现的转矩幅值上限。 + + :param w_M: 机械角速度 (rad/s) + :param v_bus: 母线电压 (V) + :return: 转矩幅值上限 (Nm) + """ + w_abs = abs(w_M) + w_e = self.n_p * w_abs + + if w_e < self._EPSILON: + return float(max(0.0, abs(self._torque_from_currents(0.0, self.i_s_max)))) + + # 优化: 减少网格点数从40到12,使用更智能的搜索策略 + i_d_estimate = max(-self.i_s_max, min(0.0, -0.5 * (self.psi_f / (self.L_q - self.L_d)) if self.L_q != self.L_d else 0.0)) + i_d_grid = np.array([ + i_d_estimate - 0.3 * self.i_s_max, + i_d_estimate - 0.2 * self.i_s_max, + i_d_estimate - 0.1 * self.i_s_max, + i_d_estimate, + i_d_estimate + 0.1 * self.i_s_max, + -0.8 * self.i_s_max, + -0.6 * self.i_s_max, + -0.4 * self.i_s_max, + -0.2 * self.i_s_max, + -0.1 * self.i_s_max, + 0.0, + -self.i_s_max, + ]) + i_d_grid = np.clip(i_d_grid, -self.i_s_max, 0.0) + + tau_best = 0.0 + for i_d in i_d_grid: + i_q = self._max_iq_given_id(i_d, w_e, v_bus) + tau_candidate = abs(self._torque_from_currents(i_d, i_q)) + if tau_candidate > tau_best: + tau_best = tau_candidate + if i_d < -0.01 * self.i_s_max: + continue + + tau_p_max = self.P_rate / max(w_abs, self._W_MIN_RAD) + return float(max(0.0, min(tau_best, tau_p_max, self._TORQUE_OVERRATE * self.tau_rate))) + + def _apply_power_slew(self, p_bus_req_kw: float, dt: float) -> float: + """对母线请求功率施加斜率限制。""" + if self.p_bus_slew_rate_kw <= 0: + self._p_bus_req_prev = float(p_bus_req_kw) + return float(p_bus_req_kw) + + delta = self.p_bus_slew_rate_kw * dt + self._p_bus_req_prev = float(p_bus_req_kw) + return float(p_bus_req_kw) + + def _power_to_torque_ref(self, p_bus_w: float, w_M: float) -> float: + """ + 将母线侧实际电气功率转换为物理电磁转矩。 + + :param p_bus_w: 实际输入给电机的电功率 (W) + :param w_M: 当前机械角速度 (rad/s) + :return: 转化为的轴向电磁阻力/推力 (Nm) + """ + if abs(p_bus_w) < 1e-9: + return 0.0 + + w_eff = max(abs(w_M), 50.0) + if p_bus_w >= 0: + # 取电模式:从母线取有功,经过内部热量/电磁损耗后,输出转动推力(负转矩) + return -(p_bus_w * self.eta_mot) / w_eff + else: + # 发电模式:电机受到负负载强推,需要输出极大的正机械阻力(查表吸收电能) + return -p_bus_w / (w_eff * max(self.eta_gen, 1e-6)) + + def _current_ref_from_torque(self, t_motor_ref: float, w_M: float, v_bus: float) -> tuple[float, float]: + """ + 将转矩参考值转换为 d/q 轴电流参考值。 + + :param t_motor_ref: 目标电机转矩 (Nm) + :param w_M: 当前机械角速度 (rad/s) + :param v_bus: 当前母线电压 (V) + :return: `(i_d_ref, i_q_ref)`,单位 A + """ + if abs(self.psi_f) < 1e-9: + return 0.0, 0.0 + + w_e = self.n_p * abs(w_M) + i_q_ref = t_motor_ref / (1.5 * self.n_p * self.psi_f) + i_q_ref = float(np.clip(i_q_ref, -self.i_s_max, self.i_s_max)) + i_d_ref = 0.0 + + if w_e < 1e-6: + return i_d_ref, i_q_ref + + _, _, u_mag = self._compute_voltage(i_d_ref, i_q_ref, w_M, v_bus) + if u_mag <= v_bus * self.k_mod: + return i_d_ref, i_q_ref + + iq_sign = 1.0 if i_q_ref >= 0 else -1.0 + iq_abs_target = abs(i_q_ref) + # 优化: 减少网格点数从80到20 + for i_d in np.linspace(-self.i_s_max, 0.0, 20): + iq_max = self._max_iq_given_id(i_d, w_e, v_bus) + if iq_max >= iq_abs_target: + return float(i_d), float(iq_sign * iq_abs_target) + + i_d_fw = -self.i_s_max + i_q_fw = iq_sign * self._max_iq_given_id(i_d_fw, w_e, v_bus) + return float(i_d_fw), float(i_q_fw) + + def _compute_loss_terms(self, w_M: float) -> tuple[float, float, float, float]: + """计算铜耗、铁耗、逆变器损耗和总损耗。""" + w_e = self.n_p * abs(w_M) + p_cu = 1.5 * self.R_s * (self.i_d**2 + self.i_q**2) + flux_ratio = ((self.psi_f + self.L_d * self.i_d) ** 2 + (self.L_q * self.i_q) ** 2) / max(self.psi_f**2, 1e-9) + p_fe = self.k_fe * (w_e**2) * flux_ratio + p_inv = 40.0 + self.k_inv * abs(self.u_d * self.i_d + self.u_q * self.i_q) + p_total = p_cu + p_fe + self.P_const_loss + p_inv + return float(p_cu), float(p_fe), float(p_inv), float(p_total) + + def _compute_bus_quantities(self, t_motor: float, w_M: float, v_bus: float) -> tuple[float, float, float, float, float]: + """ + 执行电机接口侧电气边界计算及内模功率流映射。 + + 遵循系统全生命周期能量守恒定理进行功率平衡演算是仿真有效性的核心判据。 + + :param t_motor: 瞬时电磁转矩工作点指令值 (Nm) + :param w_M: 工作侧机械角速度反馈量 (rad/s) + :param v_bus: 回环观测的母线直流电压极限 (V) + :return: 跨域功率流核心元组 `(p_shaft, p_bus, i_bus, v_motor, p_loss)` + """ + p_shaft = t_motor * w_M + + # 电磁边界与核心拓扑节点分布损耗推算 + p_cu, p_fe, p_inv, p_loss_model = self._compute_loss_terms(w_M) + p_loss = max(0.0, float(p_loss_model)) + + # 根据能量守恒计算母线功率(包含了真实的各类损耗,取代了定效率查表) + p_bus = -p_shaft + p_loss + + # 真实的有向母线电流(正:从母线流入,负:流出母线) + i_bus_real = p_bus / max(v_bus, 1e-6) + + # 遵循原有接口要求,i_bus 仅输出量级 + i_bus = abs(i_bus_real) + + # 等效电机端电势:引入合理的直流侧等效内阻(如线缆、逆变器直流侧等效压降) + # R_dc 表征母线到真正电机控制端口的线路电阻 + R_dc = 0.05 + + # 基于基尔霍夫电压定律: + # V_bus = V_motor + I_bus_real * R_dc => V_motor = V_bus - I_bus_real * R_dc + v_motor = v_bus - i_bus_real * R_dc + + # 防止系统极端情况产生非物理结果 + v_motor = float(np.clip(v_motor, 0.0, 2.0 * v_bus)) + + return float(p_shaft), float(p_bus), float(i_bus), float(v_motor), float(p_loss) + + def step( + self, + dt: float, + n_setpoint: float, + p_bus_actual_kw: float, + v_bus: float, + t_load: float, + t_ext: float, + ) -> dict: + """ + 执行电机系统单步动力学仿真。 + + 该方法基于功率流架构,根据转速控制闭环计算母线功率需求,并根据实际获得的母线功率 + 反算电磁转矩,进而更新机械状态。 + + 能量流向说明 (Sign Convention): + - 功率 (Power): + - p > 0: 电机从母线吸收电能 (电动模式 Motor Mode) + - p < 0: 电机向母线回馈电能 (发电模式 Generator Mode) + - 转矩 (Torque): + - t < 0: 驱动转矩 (助推,使转速增加) + - t > 0: 负载转矩 (阻碍,使转速衰减) + + :param dt: 仿真步长 (s) + :param n_setpoint: 目标转速指令 (RPM) + :param p_bus_actual_kw: 实际分配给电机的母线功率 (kW)。若为正,表示电池放电供电机;若为负,表示电机发电充电池。 + :param v_bus: 当前直流母线电压 (V) + :param t_load: 轴系负载转矩 (Nm)。通常为正值,表示螺旋桨/风扇的气动阻力。 + :param t_ext: 外部施加转矩 (Nm)。如涡轴发动机并联扭矩。遵循上述符号约定 (负为助推,正为阻碍)。 + :return: 包含当前仿真步所有状态变量的字典。 + """ + if dt <= 0: + raise ValueError('dt must be positive') + if v_bus <= self._EPSILON: + raise ValueError('v_bus must be positive') + + # 更新滤波系数缓存 + self._update_filter_coefficients(dt) + + self._v_bus_f += (float(v_bus) - self._v_bus_f) * self._a_v + self.u_dc = float(self._v_bus_f) + self.u_s_max = self.u_dc * self.k_mod + + t_lim = self._compute_tau_limit(self.w_M, self.u_dc) + + w_set_cmd = n_setpoint * 2 * np.pi / 60.0 + self._w_set_f += (w_set_cmd - self._w_set_f) * self._a_n + w_set = self._w_set_f + + w_error = w_set - self.w_M + if abs(w_error) < self._W_ERROR_DEADBAND: + w_error = 0.0 + + dw_error = (w_error - self._w_error_prev) / max(dt, self._EPSILON) + self._w_error_prev = w_error + self._dw_error_f = self._dw_error_f * (1 - self._DW_ERROR_FILTER) + dw_error * self._DW_ERROR_FILTER + + self._w_error_int += w_error * dt + p_lim_kw = (t_lim * max(self.w_M, self._W_MIN_RAD)) / 1000.0 / max(self.eta_mot, self._EPSILON) + int_limit = 1.2 * p_lim_kw / max(self.k_i_w, self._EPSILON) if self.k_i_w > 0 else 0.0 + self._w_error_int = np.clip(self._w_error_int, -int_limit, int_limit) + + self._t_load_f += (float(t_load) - self._t_load_f) * self._a_t_load + self._t_ext_f += (float(t_ext) - self._t_ext_f) * self._a_t_load + + # 阻力前馈折算为补偿所需电功率(kW) + t_ff = self._t_load_f + self._t_ext_f + self.B_visc * self.w_M + p_mech_ff_kw = (t_ff * max(self.w_M, 1e-3)) / 1000.0 + if p_mech_ff_kw > 0: + p_elec_ff_kw = p_mech_ff_kw / max(self.eta_mot, self._EPSILON) + else: + p_elec_ff_kw = p_mech_ff_kw * self.eta_gen + + # 阻力前馈折算为补偿所需电功率(kW) + t_ff = self._t_load_f + self._t_ext_f + self.B_visc * self.w_M + p_mech_ff_kw = (t_ff * max(self.w_M, 1e-3)) / 1000.0 + if p_mech_ff_kw > 0: + p_elec_ff_kw = p_mech_ff_kw / max(self.eta_mot, 1e-6) + else: + p_elec_ff_kw = p_mech_ff_kw * self.eta_gen + + # ------------------------------------------------------------- + # A) 需求生成层: 以转速环计算下一拍应向电池索取的 P_bus_req_kw + # ------------------------------------------------------------- + # 使用增量式PID计算控制量 + self.speed_pid.dt = dt + p_pid = self.speed_pid.compute(setpoint=w_set, measurement=self.w_M) + + # 添加微分前馈和阻力前馈 + p_d_ff = self.k_d_w_error * self._dw_error_f + + p_cmd_raw = p_pid + p_d_ff + p_elec_ff_kw + p_bus_req_kw = float(np.clip(p_cmd_raw, -p_lim_kw, p_lim_kw)) + p_bus_req_kw = self._apply_power_slew(p_bus_req_kw, dt) + + self.p_bus_req_kw = p_bus_req_kw + + # ------------------------------------------------------------- + # B) 实施层: 以外部传进来的实际被满足电能,硬算电磁反抗力和扭矩 + # ------------------------------------------------------------- + t_motor_ref = self._power_to_torque_ref(p_bus_actual_kw * 1000.0, self.w_M) + t_motor_ref = float(np.clip(t_motor_ref, -t_lim, t_lim)) + + self._t_ref_f += (t_motor_ref - self._t_ref_f) * self._a_t + t_motor_ref_filtered = float(self._t_ref_f) + + i_d_ref, i_q_ref = self._current_ref_from_torque(t_motor_ref_filtered, self.w_M, self.u_dc) + self.i_d += (i_d_ref - self.i_d) * self._alpha_i + self.i_q += (i_q_ref - self.i_q) * self._alpha_i + i_abs = np.hypot(self.i_d, self.i_q) + if i_abs > self.i_s_max > 0: + self.i_d *= self.i_s_max / i_abs + self.i_q *= self.i_s_max / i_abs + + self.t_motor = self._torque_from_currents(self.i_d, self.i_q) + self.i_s = complex(self.i_d, self.i_q) + self.u_d, self.u_q, self.u_s = self._compute_voltage(self.i_d, self.i_q, self.w_M, self.u_dc) + + d_w = -(self.t_motor + t_ext + t_load + self.B_visc * self.w_M) / max(self.J, 1e-9) + d_w = float(np.clip(d_w, -self._dw_max, self._dw_max)) + + w_prev = self.w_M + self.w_M += d_w * dt + self.w_M = max(0.0, self.w_M) + + self.p_shaft, self.p_bus, self.i_bus, self.v_motor, self.p_loss = self._compute_bus_quantities( + self.t_motor, self.w_M, self.u_dc + ) + self.theta_e += self.n_p * self.w_M * dt + self._sim_t += dt + + # =========== 逆变换与 SVPWM (提高物理级仿真保真度) =========== + # DQ -> Alpha Beta + u_alpha, u_beta = self.inv_park_transform(self.u_d, self.u_q, self.theta_e) + i_alpha, i_beta = self.inv_park_transform(self.i_d, self.i_q, self.theta_e) + + # Alpha Beta -> ABC + u_a_ideal, u_b_ideal, u_c_ideal = self.inv_clarke_transform(u_alpha, u_beta) + self.i_a, self.i_b, self.i_c = self.inv_clarke_transform(i_alpha, i_beta) + + # 实际占空比及共模注入调制的相电压 + self.duty_a, self.duty_b, self.duty_c, self.u_an, self.u_bn, self.u_cn = self.svpwm( + u_a_ideal, u_b_ideal, u_c_ideal, self.u_dc + ) + # ============================================================== + + # 添加诊断级平衡校核 + self._torque_balance_error = self.t_motor + t_ext + t_load + self.B_visc * w_prev + self.J * d_w + self._power_balance_error = self.p_bus + self.p_shaft - self.p_loss + + mode = 'idle' + if self.t_motor < -1e-9: + mode = 'motor' + elif self.t_motor > 1e-9: + mode = 'generator' + + # 内存管理:如果设置了最大日志长度,超出时删除最早的记录 + if self._log_maxlen is not None and len(self._log['t']) >= self._log_maxlen: + for key in self._log: + if len(self._log[key]) > 0: + self._log[key].pop(0) + + self._log['t'].append(self._sim_t) + self._log['w_M'].append(self.w_M) + self._log['n_rpm'].append(self.w_M * 60.0 / (2 * np.pi)) + self._log['t_motor'].append(self.t_motor) + self._log['t_load'].append(t_load) + self._log['t_ext'].append(t_ext) + self._log['p_bus_req_kw'].append(self.p_bus_req_kw) + self._log['p_bus_kw'].append(self.p_bus / 1000.0) + self._log['p_shaft_kw'].append(self.p_shaft / 1000.0) + self._log['p_loss_kw'].append(self.p_loss / 1000.0) + self._log['i_bus_a'].append(self.i_bus) + self._log['v_bus'].append(self.u_dc) + self._log['v_motor'].append(self.v_motor) + self._log['mode'].append(mode) + self._log['i_a'].append(self.i_a) + self._log['i_b'].append(self.i_b) + self._log['i_c'].append(self.i_c) + self._log['u_an'].append(self.u_an) + self._log['u_bn'].append(self.u_bn) + self._log['u_cn'].append(self.u_cn) + + return self.get_state() + + def get_state(self) -> dict: + """获取当前瞬时状态量。""" + return { + 't': self._sim_t, + 'w_rad_s': self.w_M, + 'n_rpm': self.w_M * 60.0 / (2 * np.pi), + 't_motor': self.t_motor, + 'p_bus_req_kw': getattr(self, 'p_bus_req_kw', 0.0), + 'torque_balance_error': getattr(self, '_torque_balance_error', 0.0), + 'power_balance_error': getattr(self, '_power_balance_error', 0.0), + 'p_bus_w': self.p_bus, + 'p_bus_kw': self.p_bus / 1000.0, + 'p_shaft_w': self.p_shaft, + 'p_shaft_kw': self.p_shaft / 1000.0, + 'p_loss_w': self.p_loss, + 'p_loss_kw': self.p_loss / 1000.0, + 'i_bus_a': self.i_bus, + 'v_bus': self.u_dc, + 'v_motor': self.v_motor, + 'u_s': self.u_s, + 'mode': 'motor' if self.t_motor < -1e-9 else 'generator' if self.t_motor > 1e-9 else 'idle', + 'i_a': self.i_a, 'i_b': self.i_b, 'i_c': self.i_c, + 'u_an': self.u_an, 'u_bn': self.u_bn, 'u_cn': self.u_cn, + 'duty_a': self.duty_a, 'duty_b': self.duty_b, 'duty_c': self.duty_c, + } + + def reset_log(self) -> None: + """清空日志并重置内部动态状态。""" + for key in self._log: + self._log[key] = [] + self.w_M = 0.0 + self.t_motor = 0.0 + self.p_bus = 0.0 + self.p_shaft = 0.0 + self.p_loss = 0.0 + self.i_bus = 0.0 + self.v_motor = self.u_dc + self.i_d = 0.0 + self.i_q = 0.0 + self.i_s = 0j + self.u_d = 0.0 + self.u_q = 0.0 + self.u_s = 0.0 + self.theta_e = 0.0 + self.i_a, self.i_b, self.i_c = 0.0, 0.0, 0.0 + self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0 + self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5 + self._sim_t = 0.0 + # 增量式PID控制器重置 + self.speed_pid.reset(initial_output=0.0) + self._w_error_int = 0.0 + self._w_error_prev = 0.0 + self._dw_error_f = 0.0 + self._p_bus_req_prev = 0.0 + self._w_set_f = 0.0 + self._t_ref_f = 0.0 + self._v_bus_f = float(self.u_dc) + # 负载转矩前馈相关状态变量 + self._t_load_f = 0.0 + self._t_ext_f = 0.0 + + +def run_motor_test() -> Dict[str, Any]: + """基于接口的独立电机测试 - 复杂测试用例""" + import matplotlib.pyplot as plt + + print('=' * 60) + print('Motor Dynamic Test - Complex Profile') + print('=' * 60) + + # 使用优化后的PID参数 + motor = MotorSim( + P_rate=300e3, + w_rate=575.95, + J=0.8, + k_p_w=13.440362, + k_i_w=42.997816, + k_d_w=0.484660, + P_const_loss=300.0, + k_fe=4e-4, + ) + + dt = 0.02 + t_end = 100.0 + time_array = np.arange(0.0, t_end, dt) + + # 存储关键诊断数据 + torque_balance = [] + power_balance = [] + + p_actual_kw = 0.0 + for t in time_array: + # 复杂测试工况 - 多段转速变化 + 多种扰动 + # 目标转速曲线 + if t < 3.0: + n_setpoint = 0.0 + elif t < 8.0: + n_setpoint = 2000.0 + elif t < 15.0: + n_setpoint = 2800.0 + elif t < 22.0: + n_setpoint = 2200.0 + elif t < 32.0: + n_setpoint = 3500.0 + elif t < 42.0: + n_setpoint = 3800.0 + elif t < 52.0: + n_setpoint = 2800.0 + elif t < 62.0: + n_setpoint = 3000.0 + elif t < 72.0: + n_setpoint = 4000.0 + elif t < 82.0: + n_setpoint = 4200.0 + elif t < 92.0: + n_setpoint = 3200.0 + else: + n_setpoint = 3000.0 + + # 外部转矩扰动 + if t < 5.0: + t_ext = 25.0 + elif t < 12.0: + t_ext = 10.0 + elif t < 18.0: + t_ext = -30.0 + elif t < 25.0: + t_ext = 15.0 + elif t < 35.0: + t_ext = -50.0 + elif t < 45.0: + t_ext = 8.0 + elif t < 55.0: + t_ext = -20.0 + elif t < 65.0: + t_ext = 25.0 + elif t < 75.0: + t_ext = -60.0 + elif t < 85.0: + t_ext = 20.0 + else: + t_ext = 0.0 + + # 负载转矩 - 变化因子 + if t < 10.0: + t_load_factor = 1.0 + elif t < 22.0: + t_load_factor = 1.5 + elif t < 32.0: + t_load_factor = 0.8 + elif t < 42.0: + t_load_factor = 1.8 + elif t < 52.0: + t_load_factor = 1.0 + elif t < 62.0: + t_load_factor = 1.4 + elif t < 72.0: + t_load_factor = 0.9 + elif t < 82.0: + t_load_factor = 1.6 + else: + t_load_factor = 1.1 + + base_t_load = 16.0 + 0.020 * motor.w_M + 1.2e-5 * motor.w_M**2 + t_load = base_t_load * t_load_factor + + # 母线电压波动 + v_bus = 520.0 + 15.0 * np.sin(2 * np.pi * t / 8.0) + 5.0 * np.sin(2 * np.pi * t / 3.0) + + state = motor.step(dt, n_setpoint, p_actual_kw, v_bus, t_load, t_ext) + p_actual_kw = state['p_bus_req_kw'] + + torque_balance.append(state['torque_balance_error']) + power_balance.append(state['power_balance_error']) + + state = motor.get_state() + p_bus_kw = np.array(motor._log['p_bus_kw']) + p_req_kw = np.array(motor._log['p_bus_req_kw']) + p_shaft_kw = np.array(motor._log['p_shaft_kw']) + p_loss_kw = np.array(motor._log['p_loss_kw']) + n_log = np.array(motor._log['n_rpm']) + t_motor_log = np.array(motor._log['t_motor']) + t_load_log = np.array(motor._log['t_load']) + t_ext_log = np.array(motor._log['t_ext']) + v_bus_log = np.array(motor._log['v_bus']) + v_motor_log = np.array(motor._log['v_motor']) + i_bus_log = np.array(motor._log['i_bus_a']) + + # 计算性能指标 + n_setpoints = [] + for t in time_array: + if t < 3.0: + n_setpoints.append(0.0) + elif t < 8.0: + n_setpoints.append(2000.0) + elif t < 15.0: + n_setpoints.append(2800.0) + elif t < 22.0: + n_setpoints.append(2200.0) + elif t < 32.0: + n_setpoints.append(3500.0) + elif t < 42.0: + n_setpoints.append(3800.0) + elif t < 52.0: + n_setpoints.append(2800.0) + elif t < 62.0: + n_setpoints.append(3000.0) + elif t < 72.0: + n_setpoints.append(4000.0) + elif t < 82.0: + n_setpoints.append(4200.0) + elif t < 92.0: + n_setpoints.append(3200.0) + else: + n_setpoints.append(3000.0) + n_setpoints = np.array(n_setpoints) + + error = n_setpoints - n_log + ise = np.sum(error**2) * dt + iae = np.sum(np.abs(error)) * dt + + print(f"Final: speed={state['n_rpm']:.0f} RPM ({state['w_rad_s']:.1f} rad/s)") + print(f" t_motor={state['t_motor']:.2f} Nm, p_bus={state['p_bus_kw']:.2f} kW, v_motor={state['v_motor']:.1f} V") + print(f"Performance - ISE: {ise:.2f}, IAE: {iae:.2f}") + + # 诊断结果 + torque_balance = np.array(torque_balance) + power_balance = np.array(power_balance) + print(f"Diagnostic - Torque balance error (mean): {np.mean(np.abs(torque_balance)):.4f} Nm") + print(f"Diagnostic - Power balance error (mean): {np.mean(np.abs(power_balance)):.4f} W") + + fig, axes = plt.subplots(5, 1, figsize=(14, 14), sharex=True) + + axes[0].plot(time_array, n_setpoints, 'r--', lw=1.0, alpha=0.7, label='Setpoint') + axes[0].plot(time_array, n_log, 'b-', lw=1.6, label='Actual') + axes[0].set_ylabel('Speed [RPM]') + axes[0].set_title('Motor Speed - Complex Profile Test (100s)') + axes[0].grid(True, linestyle=':') + axes[0].legend() + + axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque') + axes[1].plot(time_array, t_load_log, 'k--', lw=1.0, label='Load Torque') + axes[1].plot(time_array, t_ext_log, color='tab:purple', lw=1.0, label='External Torque') + axes[1].set_ylabel('Torque [Nm]') + axes[1].set_title('Torque Balance') + axes[1].axhline(0.0, color='k', lw=0.6) + axes[1].grid(True, linestyle=':') + axes[1].legend() + + axes[2].plot(time_array, p_req_kw, color='tab:gray', lw=1.2, label='Requested Bus Power') + axes[2].plot(time_array, p_bus_kw, color='m', lw=1.4, label='Actual Bus Power') + axes[2].plot(time_array, p_shaft_kw, 'g-', lw=1.2, label='Shaft Power') + axes[2].plot(time_array, p_loss_kw, color='tab:orange', lw=1.0, label='Loss Power') + axes[2].set_ylabel('Power [kW]') + axes[2].set_title('Bus / Shaft / Loss Power') + axes[2].axhline(0.0, color='k', lw=0.6) + axes[2].grid(True, linestyle=':') + axes[2].legend() + + axes[3].plot(time_array, v_bus_log, color='tab:cyan', lw=1.4, label='Bus Voltage') + axes[3].plot(time_array, v_motor_log, color='tab:red', lw=1.2, label='Motor Potential') + axes[3].set_ylabel('Voltage [V]') + axes[3].set_title('Bus Voltage and Motor Equivalent Potential') + axes[3].grid(True, linestyle=':') + axes[3].legend() + + axes[4].plot(time_array, i_bus_log, color='tab:brown', lw=1.4, label='Bus Current') + axes[4].set_ylabel('Current [A]') + axes[4].set_xlabel('Time [s]') + axes[4].set_title('Bus Current') + axes[4].grid(True, linestyle=':') + axes[4].legend() + + plt.tight_layout() + plt.savefig("figures/test_complex_motor_output.png") + + return { + 'final_state': state, + 'peak_speed_rpm': float(np.max(n_log)), + 'peak_bus_current_a': float(np.max(i_bus_log)), + 'torque_balance_error_mean': float(np.mean(np.abs(torque_balance))), + 'power_balance_error_mean': float(np.mean(np.abs(power_balance))), + 'ise': ise, + 'iae': iae, + } + + +def run_motor_battery_coupled_test() -> Dict[str, Any]: + """执行混合动力电机与电池协同闭环抗扰动验证例程 - 复杂测试用例""" + import matplotlib + matplotlib.use('Agg') # 强制使用非交互后端 + import matplotlib.pyplot as plt + import numpy as np + + from src.battery_sim import BatterySim + + print('=' * 60) + print('Motor + Battery Co-Simulation Test (100s) - Complex Profile') + print('=' * 60) + + battery = BatterySim(capacity_kwh=50.0, nom_voltage=520.0, initial_soc=0.65) + battery.V_t = float(battery._get_ocv(battery.SOC)) + + motor = MotorSim( + P_rate=300e3, + w_rate=575.95, + J=0.8, + k_p_w=13.440362, + k_i_w=42.997816, + k_d_w=0.484660, + P_const_loss=300.0, + k_fe=4e-4, + u_dc=battery.V_t, + ) + + dt = 0.02 + t_end = 100.0 + time_array = np.arange(0.0, t_end, dt) + + soc_log = [] + p_req_log = [] + p_bus_log = [] + p_ext_elec_log = [] + p_net_log = [] + v_bus_log = [] + n_setpoints = [] + t_ext_log = [] + t_load_log = [] + + p_actual_kw = 0.0 + for t in time_array: + # 复杂测试工况 - 多段转速变化 + 多种扰动 + p_ext_elec = 0.0 # 外部电功率扰动 (kW) + + # 目标转速曲线 - 复杂多段变化 + if t < 3.0: + n_setpoint = 0.0 + elif t < 8.0: + n_setpoint = 2000.0 + elif t < 15.0: + n_setpoint = 2800.0 + elif t < 22.0: + n_setpoint = 2200.0 + elif t < 32.0: + n_setpoint = 3500.0 + elif t < 42.0: + n_setpoint = 3800.0 + elif t < 52.0: + n_setpoint = 2800.0 + elif t < 62.0: + n_setpoint = 3000.0 + elif t < 72.0: + n_setpoint = 4000.0 + elif t < 82.0: + n_setpoint = 4200.0 + elif t < 92.0: + n_setpoint = 3200.0 + else: + n_setpoint = 3000.0 + + # 外部转矩扰动 - 模拟发动机并联/涡轴输出变化 + if t < 5.0: + t_ext = 25.0 + elif t < 12.0: + t_ext = 10.0 + elif t < 18.0: + t_ext = -30.0 + elif t < 25.0: + t_ext = 15.0 + elif t < 35.0: + t_ext = -50.0 + elif t < 45.0: + t_ext = 8.0 + elif t < 55.0: + t_ext = -20.0 + elif t < 65.0: + t_ext = 25.0 + elif t < 75.0: + t_ext = -60.0 + elif t < 85.0: + t_ext = 20.0 + else: + t_ext = 0.0 + + # 负载转矩 - 模拟风速/气压变化 + if t < 10.0: + t_load = 20.0 + elif t < 22.0: + t_load = 80.0 + elif t < 32.0: + t_load = 40.0 + elif t < 42.0: + t_load = 120.0 + elif t < 52.0: + t_load = 50.0 + elif t < 62.0: + t_load = 90.0 + elif t < 72.0: + t_load = 60.0 + elif t < 82.0: + t_load = 130.0 + else: + t_load = 70.0 + + # 外部电功率扰动 + if 35.0 < t < 40.0: + p_ext_elec = 80.0 # 涡轴发电机注入 80kW + elif 55.0 < t < 60.0: + p_ext_elec = -100.0 # 其他设备用电 100kW + elif 75.0 < t < 80.0: + p_ext_elec = 60.0 # 涡轴发电机注入 60kW + + # 母线电压波动 + v_bus = battery.V_t + 15.0 * np.sin(2 * np.pi * t / 8.0) + 5.0 * np.sin(2 * np.pi * t / 3.0) + + # 延迟一拍的实际电能参与当前动态步,进而PID在内环打出对下一拍的请求 + state = motor.step(dt, n_setpoint, p_actual_kw, v_bus, t_load, t_ext) + p_req_kw = state['p_bus_req_kw'] + + # 计算汇流排给电池真实指派的载荷 = 电机需求 - 其他设备并网干扰 + p_net_req_kw = p_req_kw - p_ext_elec + + # 电池响应,输出真正被释放出来的电功率 + p_net_actual_kw, _, _, soc = battery.step(dt, p_net_req_kw) + + # 回算到电机引脚真正获得的被供应功率,送给下个脉冲周期 + p_actual_kw = p_net_actual_kw + p_ext_elec + + n_setpoints.append(n_setpoint) + soc_log.append(soc) + p_req_log.append(p_req_kw) + p_ext_elec_log.append(p_ext_elec) + p_net_log.append(p_net_req_kw) + p_bus_log.append(p_net_actual_kw) + v_bus_log.append(v_bus) + t_ext_log.append(t_ext) + t_load_log.append(t_load) + + n_log = np.array(motor._log['n_rpm']) + t_motor_log = np.array(motor._log['t_motor']) + v_motor_log = np.array(motor._log['v_motor']) + + # 计算性能指标 + n_setpoints = np.array(n_setpoints) + error = n_setpoints - n_log + ise = np.sum(error**2) * dt + iae = np.sum(np.abs(error)) * dt + + print(f"Final: speed={motor.get_state()['n_rpm']:.0f} RPM, SOC={soc_log[-1]*100:.3f}%") + print(f"Performance - ISE: {ise:.2f}, IAE: {iae:.2f}") + + fig, axes = plt.subplots(5, 1, figsize=(14, 14), sharex=True) + + # 1. 速度响应 + axes[0].plot(time_array, n_setpoints, 'r--', lw=1.2, label='Speed Ref (Setpoint)') + axes[0].plot(time_array, n_log, 'b-', lw=1.6, label='Actual Speed') + axes[0].set_ylabel('Speed [RPM]') + axes[0].set_title('Motor Speed Tracking - Complex Profile') + axes[0].grid(True, linestyle=':') + axes[0].legend() + + # 2. 转矩响应 + axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque (PID Output)') + axes[1].plot(time_array, np.array(t_load_log), 'k--', lw=1.2, label='Propeller Load Torque') + axes[1].plot(time_array, np.array(t_ext_log), color='tab:purple', lw=1.2, label='External Torque') + axes[1].axhline(0.0, color='k', lw=0.6) + axes[1].set_ylabel('Torque [Nm]') + axes[1].set_title('Torque Balance') + axes[1].grid(True, linestyle=':') + axes[1].legend() + + # 3. 功率响应 + axes[2].plot(time_array, np.array(p_req_log), 'm-', lw=1.6, label='Motor Requested Bus Power') + axes[2].plot(time_array, np.array(p_ext_elec_log), 'g--', lw=1.2, label='Engine Gen Power') + axes[2].plot(time_array, np.array(p_net_log), 'k-', lw=1.2, label='Battery Net Output Power') + axes[2].axhline(0.0, color='k', lw=0.6) + axes[2].set_ylabel('Power [kW]') + axes[2].set_title('Bus Power Balance & Electrical Source Injection') + axes[2].grid(True, linestyle=':') + axes[2].legend() + + # 4. 电压响应 + axes[3].plot(time_array, np.array(v_bus_log), color='tab:cyan', lw=1.4, label='Bus Voltage') + axes[3].plot(time_array, v_motor_log, color='tab:red', lw=1.2, label='Motor Potential') + axes[3].set_ylabel('Voltage [V]') + axes[3].set_title('Bus Voltage & Motor Potential') + axes[3].grid(True, linestyle=':') + axes[3].legend() + + # 5. SOC响应 + axes[4].plot(time_array, np.array(soc_log) * 100, color='tab:green', lw=1.4, label='Battery SOC') + axes[4].set_ylabel('SOC [%]') + axes[4].set_xlabel('Time [s]') + axes[4].set_title('Battery State of Charge') + axes[4].grid(True, linestyle=':') + axes[4].legend() + + plt.tight_layout() + plt.savefig("figures/test_complex_output.png") + print("Plot saved to figures/test_complex_output.png") + + return { + 'final_state': motor.get_state(), + 'final_soc': float(soc_log[-1]), + 'peak_speed_rpm': float(np.max(n_log)), + 'ise': ise, + 'iae': iae, + } + +if __name__ == '__main__': + run_motor_battery_coupled_test() diff --git a/Model/src/series_hybrid_sim.py b/Model/src/series_hybrid_sim.py new file mode 100644 index 0000000..7071cd1 --- /dev/null +++ b/Model/src/series_hybrid_sim.py @@ -0,0 +1,261 @@ +import os +import numpy as np +import matplotlib.pyplot as plt + +from engine_dynamic_sim import TurboshaftDynamicSim +from motor_sim import MotorSim +from battery_sim import BatterySim + +class SeriesHybridSystem: + """ + 串联式混合动力系统总成 + """ + def __init__(self): + # ===== 新增:按Model目录定位GPR数据与权重,避免从主项目调用时路径错误 ===== + model_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + gpr_csv_path = os.path.join(model_root, "data", "Cleaned_Engine_Data_Full.csv") + gpr_pth_path = os.path.join(model_root, "data", "engine_gpr_model.pth") + self.genset = TurboshaftDynamicSim( + gpr_csv_path=gpr_csv_path, + gpr_pth_path=gpr_pth_path, + kp=0.5, + ki=0.5, + kd=0.0 + ) + self.drive_motor = MotorSim( + P_rate=300e3, + w_rate=575.95, + k_p_w=5.0, # 增大比例增益 + k_i_w=2.0, # 增大积分增益 + k_d_w=0.5, # 适度微分 + J=1.0, + ) + self.battery = BatterySim(capacity_kwh=50.0, initial_soc=0.6) + + self.bus_voltage = self.battery._get_ocv(self.battery.SOC) + self.motor_actual_power_kw = 0.0 + + # 滞环控制状态 + self._genset_low_power_mode = False # 发动机低功率模式标志 + self._hysteresis_rpm = 100 # 滞环带宽 RPM + + def energy_management_strategy(self, p_drive_req_kw, soc, speed_error_rpm=0.0): + """ + 功率跟随策略 (Power Following Control) - 增强版 + 核心思想: + - 使用滞环控制避免频繁切换 + - 当转速持续高于目标时,降低发动机功率 + - 当转速持续低于目标时,增加发动机功率 + """ + P_eng_max = 300.0 + P_eng_min = 20.0 + SOC_TARGET = 0.60 + K_soc = 30.0 # kW / ΔSOC + K_speed = 1.5 # kW / RPM + p_dyn_reserve = P_eng_max * 0.3 # 90 kW 基础储备 + + # 滞环控制逻辑 + # 进入低功率模式:转速高于目标 + 电机发电 + if speed_error_rpm < -self._hysteresis_rpm and p_drive_req_kw < 0: + self._genset_low_power_mode = True + # 退出低功率模式:转速低于目标 - 滞环带宽 + elif speed_error_rpm > self._hysteresis_rpm: + self._genset_low_power_mode = False + + # 根据模式计算目标功率 + if self._genset_low_power_mode: + # 低功率模式:发动机降到最低 + target_engine_power = P_eng_min + elif speed_error_rpm > 0: + # 加速模式 + p_speed_comp = min(speed_error_rpm * K_speed, 250.0) + target_engine_power = p_drive_req_kw + p_dyn_reserve + p_speed_comp + else: + # 减速/稳态模式 + p_speed_comp = max(speed_error_rpm * K_speed, -250.0) + target_engine_power = p_drive_req_kw + p_dyn_reserve + p_speed_comp + + # SOC补偿 + soc_error = SOC_TARGET - soc + if soc < 0.2 or soc > 0.85: + K_soc_effective = 0 + else: + K_soc_effective = K_soc + p_charge_req = soc_error * K_soc_effective + target_engine_power += p_charge_req + + # 极限状态越界保护 + if soc < 0.15: + target_engine_power = P_eng_max + elif soc > 0.95: + target_engine_power = P_eng_min + + # 限制输出范围 + target_engine_power = max(P_eng_min, min(P_eng_max, target_engine_power)) + + return target_engine_power + + def step(self, dt, target_prop_speed, prop_load_torque): + """ + 全系统单步动力学仿真 + """ + # 1. 需求端:驱动电机电功率请求 + motor_state = self.drive_motor.step( + dt=dt, + n_setpoint=target_prop_speed, + p_bus_actual_kw=self.motor_actual_power_kw, + v_bus=self.bus_voltage, + t_load=prop_load_torque, + t_ext=0.0 + ) + p_drive_req = motor_state['p_bus_req_kw'] + actual_rpm = motor_state['n_rpm'] + + # 计算转速误差(用于EMS) + speed_error_rpm = target_prop_speed - actual_rpm + + # 2. 决策端:EMS 目标功率计算(加入转速误差反馈) + target_engine_pwr = self.energy_management_strategy(p_drive_req, self.battery.SOC, speed_error_rpm) + + # 3. 发电端:涡轴发动机响应并输出电能 + N_eng, Wf_act, Wf_req, P_eng_out = self.genset.step(dt, target_power=target_engine_pwr) + # GPR模型输出的 P_eng_out > 0 表示发动机输出功率(发电) + p_gen_elec = P_eng_out # 正值表示发电功率 + + # 4. 汇流端:电池功率补偿与直流母线状态更新 + # 功率平衡:电机需求 = 发动机发电 + 电池补充 + # p_batt_req > 0 表示电池放电,p_batt_req < 0 表示电池充电 + p_batt_req = p_drive_req - p_gen_elec + p_batt_actual, v_bus, i_batt, soc = self.battery.step(dt, p_batt_req) + + # 5. 状态记忆:为打破代数环,保留关键变量至下一拍 + self.bus_voltage = v_bus + + # 电机实际获得的功率 = 发电功率 + 电池放电功率 + # 注意:允许负值存在,表示电机在发电模式(制动) + # 这样电机控制器才能正确产生制动转矩 + self.motor_actual_power_kw = p_gen_elec + p_batt_actual + + return { + 'soc': soc * 100.0, + 'bus_voltage': v_bus, + 'prop_speed_rpm': motor_state['n_rpm'], + 'target_engine_pwr': target_engine_pwr, + 'p_engine_out_kw': P_eng_out, + 'p_drive_req_kw': p_drive_req, + 'p_motor_actual_kw': self.motor_actual_power_kw, + 'p_batt_actual_kw': p_batt_actual, + 'wf_kg_h': Wf_act, + 'engine_rpm': N_eng + } + + +if __name__ == "__main__": + import os + import matplotlib + matplotlib.use('Agg') # 非交互后端 + import matplotlib.pyplot as plt + + plt.rcParams['font.family'] = 'serif' + plt.rcParams['axes.unicode_minus'] = True + + print("-> 初始化串联混电系统...") + system = SeriesHybridSystem() + system.genset.set_steady_state_by_power(H_env=0.0, Ma_env=0.0, Power_target=50.0) + + dt = 0.02 + t_end = 180.0 + time_array = np.arange(0, t_end, dt) + + log = {k: [] for k in ['soc', 'bus_voltage', 'prop_speed_rpm', 'target_prop_rpm', 'target_engine_pwr', + 'p_engine_out_kw', 'p_drive_req_kw', 'p_motor_actual_kw', + 'p_batt_actual_kw', 'wf_kg_h']} + + print("-> 开始全系统闭环步进仿真 (总时长 3 分钟)...") + for t in time_array: + # 3分钟测试剖面 + if t < 15.0: + target_rpm, load_torque = 1500.0, 50.0 # 地面滑行 + elif t < 60.0: + target_rpm, load_torque = 3000.0, 200.0 # 暴力拉升 + elif t < 120.0: + target_rpm, load_torque = 2800.0, 150.0 # 重载巡航 + else: + target_rpm, load_torque = 1800.0, 60.0 # 降落滑行 + + res = system.step(dt, target_rpm, load_torque) + res['target_prop_rpm'] = target_rpm + for k in log.keys(): + log[k].append(res[k]) + + print("-> 仿真完成,正在绘制系统响应曲线...") + + # 绘图逻辑 + fig, axes = plt.subplots(4, 1, figsize=(14, 12), sharex=True) + fig.suptitle('Series Hybrid Electric System Dynamics (Load Following EMS)', fontweight='bold', fontsize=14) + + # 1. 转速响应 + axes[0].plot(time_array, log['target_prop_rpm'], 'k--', lw=1.5, label='Target Speed') + axes[0].plot(time_array, log['prop_speed_rpm'], 'b-', lw=1.5, label='Actual Speed') + axes[0].set_ylabel('Speed [RPM]') + axes[0].set_title('Drive Motor Speed Response') + axes[0].grid(True, linestyle=':', alpha=0.7) + axes[0].legend() + + # 2. 功率分配流向 + axes[1].plot(time_array, log['p_drive_req_kw'], 'k--', lw=1.5, label='Drive Motor Request') + axes[1].plot(time_array, log['p_engine_out_kw'], 'r-', lw=1.5, label='Engine Output (APU)') + axes[1].plot(time_array, log['p_batt_actual_kw'], 'g-', lw=1.5, label='Battery Output') + axes[1].set_ylabel('Power [kW]') + axes[1].set_title('System Power Flow (Energy Management)') + axes[1].axhline(0, color='gray', lw=1) + axes[1].grid(True, linestyle=':', alpha=0.7) + axes[1].legend() + + # 3. 电池状态 + axes[2].plot(time_array, log['bus_voltage'], 'm-', lw=1.5, label='DC Bus Voltage') + axes[2].set_ylabel('Voltage [V]') + axes[2].set_title('DC Bus Electrical State') + axes[2].grid(True, linestyle=':', alpha=0.7) + axes[2].legend(loc='upper left') + + ax2_soc = axes[2].twinx() + ax2_soc.plot(time_array, log['soc'], 'c--', lw=2, label='Battery SOC') + ax2_soc.set_ylabel('SOC [%]') + ax2_soc.legend(loc='upper right') + + # 4. 燃油消耗 + axes[3].plot(time_array, log['wf_kg_h'], 'tab:orange', lw=1.5, label='Engine Fuel Flow') + axes[3].set_ylabel('Fuel Flow [kg/h]') + axes[3].set_xlabel('Time [s]') + axes[3].set_title('Turboshaft Engine Fuel Consumption') + axes[3].grid(True, linestyle=':', alpha=0.7) + axes[3].legend() + + plt.tight_layout(rect=[0, 0, 1, 0.96]) + + # 保存到 ../figures 目录 + figures_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'figures') + if not os.path.exists(figures_dir): + os.makedirs(figures_dir) + save_path = os.path.join(figures_dir, 'series_hybrid_system_3min_test.png') + + plt.savefig(save_path, dpi=300) + print(f"-> 绘图已保存: {save_path}") + + # 保存数据到dat文件 + data_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data') + if not os.path.exists(data_dir): + os.makedirs(data_dir) + dat_path = os.path.join(data_dir, 'series_hybrid_data.dat') + + with open(dat_path, 'w') as f: + f.write('# Time(s)\tTarget_RPM\tActual_RPM\tError_RPM\tSOC\tEngine_Power\tDrive_Req\tBatt_Power\n') + for i in range(len(time_array)): + err = log['target_prop_rpm'][i] - log['prop_speed_rpm'][i] + f.write(f'{time_array[i]:.3f}\t{log["target_prop_rpm"][i]:.1f}\t' + f'{log["prop_speed_rpm"][i]:.1f}\t{err:.1f}\t{log["soc"][i]:.2f}\t' + f'{log["p_engine_out_kw"][i]:.2f}\t{log["p_drive_req_kw"][i]:.2f}\t' + f'{log["p_batt_actual_kw"][i]:.2f}\n') + print(f"-> 数据已保存: {dat_path}") + # plt.show() diff --git a/README.md b/README.md index ae60e2b..a97c476 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ # 🎛️ 自动控制原理AI+数智平台 -> 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·AI问答 +> 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·算例演示·AI问答 -一个基于 Gradio 构建的现代化自动控制原理学习平台,集成了系统分析工具和 AI 智能问答功能。 +一个基于 Gradio 构建的现代化自动控制原理学习平台,集成了系统分析工具、完整混动模型算例演示与 AI 智能问答功能。 ## ✨ 核心功能 @@ -54,7 +54,14 @@ - s 平面稳定性区域 - 阻尼比等值线 -### 🤖 4. AI 智能问答 (Q&A) +### 🧪 4. 算例演示 (Case Demo) + +- **完整混动模型**:调用 `Model/src/series_hybrid_sim.py` 进行系统级仿真 +- **参数化工况**:支持工况模板、仿真时长、步长、SOC、初始发动机功率与缩放系数 +- **图文结果**:输出三联图(转速响应/功率分配/电气状态)与关键数据表 +- **教学解读**:自动生成指标摘要(最大转速误差、SOC变化、平均功率与燃油流量) + +### 🤖 5. AI 智能问答 (Q&A) - **专业教学助手**:精通自动控制原理的 AI 教授 - **流式响应**:实时显示 AI 回复过程 @@ -62,14 +69,13 @@ - **上下文记忆**:支持多轮对话 **支持的 API**: -- DeepSeek API(推荐,国内网络友好) -- Google Gemini API +- DeepSeek API ## 🚀 快速开始 ### 环境要求 -- Python 3.8+ +- Python 3.10+ - pip 包管理器 ### 安装步骤 @@ -84,20 +90,20 @@ cd AutoControl 2. **安装依赖** ```bash -pip install gradio numpy control matplotlib aiohttp +pip install -r requirements.txt ``` 或使用 conda: ```bash -conda create -n autocontrol python=3.9 +conda create -n autocontrol python=3.10 conda activate autocontrol -pip install gradio numpy control matplotlib aiohttp +pip install -r requirements.txt ``` 3. **配置 API 密钥** -编辑 `app.py` 文件开头的配置区域: +编辑 `config.py` 文件中的配置区域: ```python # ==================== API 配置 ==================== @@ -240,27 +246,28 @@ AI 回复中的数学公式会自动渲染,支持以下格式: ## 📁 项目结构 -``` -AutoControl/ -├── app.py # 主应用程序 -├── README.md # 项目文档 -├── API_CONFIG.md # API 配置详细说明(可选) -├── QUICK_START.md # 快速入门指南(可选) -└── requirements.txt # 依赖列表(可选) +```text +AutoControlCourse/ +├── app.py # Gradio 入口与事件绑定 +├── ui_components.py # 各标签页 UI 组件 +├── analysis_functions.py # 时域/频域/根轨迹计算 +├── case_demo_functions.py # 算例演示模块(调用 Model) +├── chatbot.py # AI 问答 +├── user_stats.py # 在线人数统计 +├── config.py # API 与运行配置 +├── Model/ +│ ├── src/ # 混动模型核心代码 +│ └── data/ # 运行所需模型权重与数据 +└── requirements.txt # 依赖列表 ``` ## 🔧 高级配置 -### 切换到 Gemini API +### 算例演示说明 -如果您想使用 Google Gemini API: - -```python -API_KEY = "your-gemini-api-key" -API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" -API_MODEL = "gemini-1.5-flash" -API_TYPE = "gemini" -``` +- 仿真时长是模型时间,不等于程序实际等待时间 +- 电池功率符号定义:正值放电,负值充电 +- 若参数超限,程序会在安全范围内自动裁剪 ### 自定义系统提示词 @@ -333,4 +340,4 @@ ax.grid(True, alpha=0.3, linestyle='--') **⭐ 如果这个项目对您有帮助,请给它一个 Star!** -最后更新:2025年10月15日 +最后更新:2026年4月7日 diff --git a/app.py b/app.py index dcd143f..976d8ef 100644 --- a/app.py +++ b/app.py @@ -10,6 +10,8 @@ from analysis_functions import ( frequency_domain_analysis, root_locus_analysis ) +# ===== 新增:算例演示模块函数导入 ===== +from case_demo_functions import run_case_demo from chatbot import chat_with_ai from user_stats import get_online_status_html, update_user_activity from ui_components import ( @@ -17,6 +19,7 @@ from ui_components import ( create_time_domain_tab, create_frequency_domain_tab, create_root_locus_tab, + create_case_demo_tab, create_chatbot_tab ) @@ -59,7 +62,10 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus freq_domain_ui = create_frequency_domain_tab() with gr.TabItem("🎯 根轨迹 (Root Locus)", id=2): root_locus_ui = create_root_locus_tab() - with gr.TabItem("🤖 智能问答 (Q&A)", id=3): + # ===== 新增:算例演示 Tab(位于根轨迹与智能问答之间) ===== + with gr.TabItem("🧪 算例演示 (Case Demo)", id=3): + case_demo_ui = create_case_demo_tab() + with gr.TabItem("🤖 智能问答 (Q&A)", id=4): chatbot_ui = create_chatbot_tab() # 2. 绑定事件逻辑 @@ -144,6 +150,41 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus num_input.change(fn=update_all_on_tf_change, inputs=tf_change_inputs, outputs=tf_change_outputs) den_input.change(fn=update_all_on_tf_change, inputs=tf_change_inputs, outputs=tf_change_outputs) + # ===== 新增:算例演示事件包装器 ===== + def run_case_demo_wrapper(sim_time, dt, initial_soc, initial_engine_power, profile, rpm_scale, load_scale, sid): + update_user_activity(sid) + fig, summary, table_data = run_case_demo( + sim_time_s=sim_time, + dt=dt, + initial_soc_pct=initial_soc, + initial_engine_power_kw=initial_engine_power, + profile_name=profile, + rpm_scale=rpm_scale, + load_scale=load_scale + ) + return fig, summary, table_data, get_online_status_html() + + # ===== 新增:算例演示按钮事件绑定 ===== + case_demo_ui["run_button"].click( + fn=run_case_demo_wrapper, + inputs=[ + case_demo_ui["sim_time"], + case_demo_ui["dt"], + case_demo_ui["initial_soc"], + case_demo_ui["initial_engine_power"], + case_demo_ui["profile"], + case_demo_ui["rpm_scale"], + case_demo_ui["load_scale"], + session_id + ], + outputs=[ + case_demo_ui["plot"], + case_demo_ui["summary"], + case_demo_ui["table"], + online_counter + ] + ) + # --- 聊天机器人事件 --- async def chat_wrapper(message, history, sid): update_user_activity(sid) diff --git a/case_demo_functions.py b/case_demo_functions.py new file mode 100644 index 0000000..7301670 --- /dev/null +++ b/case_demo_functions.py @@ -0,0 +1,145 @@ +import os +import sys +# ===== 新增:OpenMP冲突兼容设置,避免PyTorch初始化报错 ===== +os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE") +import numpy as np +import matplotlib +matplotlib.use('Agg') +import matplotlib.pyplot as plt + +MODEL_SRC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Model", "src") +if MODEL_SRC_PATH not in sys.path: + # ===== 新增:将混动模型源码路径加入导入搜索路径 ===== + sys.path.insert(0, MODEL_SRC_PATH) + + +def _profile_points(profile_name): + if profile_name == "高机动阶跃": + return [ + (0.0, 1600.0, 70.0), + (8.0, 3200.0, 220.0), + (20.0, 2500.0, 130.0), + (35.0, 3400.0, 250.0), + (50.0, 1800.0, 80.0), + ] + if profile_name == "经济巡航": + return [ + (0.0, 1500.0, 60.0), + (15.0, 2100.0, 95.0), + (35.0, 2300.0, 105.0), + (55.0, 2000.0, 90.0), + ] + return [ + (0.0, 1500.0, 50.0), + (10.0, 3000.0, 200.0), + (30.0, 2800.0, 150.0), + (50.0, 1800.0, 60.0), + ] + + +def _target_from_profile(t, points, rpm_scale, load_scale): + rpm, torque = points[0][1], points[0][2] + for p_t, p_rpm, p_torque in points: + if t >= p_t: + rpm, torque = p_rpm, p_torque + else: + break + return rpm * rpm_scale, torque * load_scale + + +def run_case_demo(sim_time_s, dt, initial_soc_pct, initial_engine_power_kw, profile_name, rpm_scale, load_scale): + try: + try: + # ===== 新增:懒加载混动系统模型,便于捕获缺失依赖 ===== + from series_hybrid_sim import SeriesHybridSystem + except ModuleNotFoundError as e: + if getattr(e, "name", "") == "torch": + return None, "算例仿真失败:缺少依赖 torch,请先在当前环境安装 PyTorch。", [] + return None, f"算例仿真失败:缺少依赖 {e.name}。", [] + + sim_time_s = float(np.clip(sim_time_s, 10.0, 240.0)) + dt = float(np.clip(dt, 0.01, 0.2)) + initial_soc_pct = float(np.clip(initial_soc_pct, 10.0, 95.0)) + initial_engine_power_kw = float(np.clip(initial_engine_power_kw, 20.0, 260.0)) + rpm_scale = float(np.clip(rpm_scale, 0.5, 1.6)) + load_scale = float(np.clip(load_scale, 0.5, 1.6)) + + points = _profile_points(profile_name) + system = SeriesHybridSystem() + system.battery.SOC = initial_soc_pct / 100.0 + system.bus_voltage = system.battery._get_ocv(system.battery.SOC) + system.genset.set_steady_state_by_power(H_env=0.0, Ma_env=0.0, Power_target=initial_engine_power_kw) + + time_array = np.arange(0.0, sim_time_s, dt) + log = {k: [] for k in [ + "soc", "bus_voltage", "prop_speed_rpm", "target_prop_rpm", + "target_engine_pwr", "p_engine_out_kw", "p_drive_req_kw", + "p_batt_actual_kw", "wf_kg_h" + ]} + + for t in time_array: + target_rpm, load_torque = _target_from_profile(t, points, rpm_scale, load_scale) + res = system.step(dt, target_rpm, load_torque) + res["target_prop_rpm"] = target_rpm + for k in log: + log[k].append(res[k]) + + speed_error = np.array(log["target_prop_rpm"]) - np.array(log["prop_speed_rpm"]) + soc_arr = np.array(log["soc"]) + fuel_arr = np.array(log["wf_kg_h"]) + engine_pwr_arr = np.array(log["p_engine_out_kw"]) + batt_pwr_arr = np.array(log["p_batt_actual_kw"]) + + fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True) + axes[0].plot(time_array, log["target_prop_rpm"], "k--", lw=1.5, label="目标转速") + axes[0].plot(time_array, log["prop_speed_rpm"], "b-", lw=1.5, label="实际转速") + axes[0].set_ylabel("RPM") + axes[0].set_title("推进轴转速响应") + axes[0].grid(True, linestyle=":") + axes[0].legend() + + axes[1].plot(time_array, log["p_drive_req_kw"], "k--", lw=1.2, label="电机需求") + axes[1].plot(time_array, log["p_engine_out_kw"], "r-", lw=1.2, label="发动机输出") + axes[1].plot(time_array, log["p_batt_actual_kw"], "g-", lw=1.2, label="电池功率") + axes[1].axhline(0, color="gray", lw=1) + axes[1].set_ylabel("kW") + axes[1].set_title("功率分配") + axes[1].grid(True, linestyle=":") + axes[1].legend() + + axes[2].plot(time_array, log["bus_voltage"], "m-", lw=1.2, label="母线电压") + axes[2].set_ylabel("V") + axes[2].set_xlabel("时间 (s)") + axes[2].set_title("电气状态") + axes[2].grid(True, linestyle=":") + ax_soc = axes[2].twinx() + ax_soc.plot(time_array, log["soc"], "c--", lw=1.6, label="SOC") + ax_soc.set_ylabel("SOC (%)") + + fig.tight_layout() + + summary = ( + f"### 算例结果解读\n" + f"- 仿真时长:{sim_time_s:.1f} s,步长:{dt:.3f} s\n" + f"- 最大转速误差:{np.max(np.abs(speed_error)):.1f} RPM\n" + f"- SOC 变化:{soc_arr[0]:.2f}% → {soc_arr[-1]:.2f}%(最小 {np.min(soc_arr):.2f}%)\n" + f"- 平均发动机输出:{np.mean(engine_pwr_arr):.2f} kW\n" + f"- 平均电池功率:{np.mean(batt_pwr_arr):.2f} kW\n" + f"- 平均燃油流量:{np.mean(fuel_arr):.2f} kg/h" + ) + + pick_idx = np.linspace(0, len(time_array) - 1, 8, dtype=int) + table_data = [] + for idx in pick_idx: + table_data.append([ + round(float(time_array[idx]), 2), + round(float(log["target_prop_rpm"][idx]), 1), + round(float(log["prop_speed_rpm"][idx]), 1), + round(float(log["p_engine_out_kw"][idx]), 2), + round(float(log["p_batt_actual_kw"][idx]), 2), + round(float(log["soc"][idx]), 2), + ]) + + return fig, summary, table_data + except Exception as e: + return None, f"算例仿真失败:{e}", [] diff --git a/config.py b/config.py index 1b2e024..c1129f7 100644 --- a/config.py +++ b/config.py @@ -16,4 +16,4 @@ API_TYPE = "deepseek" # 当前支持 "deepseek" # ==================== Gradio 应用启动配置 ==================== SERVER_NAME = "0.0.0.0" # 监听所有网络接口 SERVER_PORT = 7860 # 指定一个端口 -SHARE = False # 是否创建Gradio的公开分享链接 +SHARE = True # 是否创建Gradio的公开分享链接 diff --git a/data_usage/case_demo_test_results.json b/data_usage/case_demo_test_results.json new file mode 100644 index 0000000..2b11413 --- /dev/null +++ b/data_usage/case_demo_test_results.json @@ -0,0 +1,70 @@ +[ + { + "name": "常规-起飞巡航", + "figure_ok": true, + "summary_ok": true, + "table_len": 8, + "summary_head": "### 算例结果解读", + "soc_min": 60.0, + "soc_max": 62.05, + "first_row": [ + 0.0, + 1500.0, + 0.0, + 50.0, + -0.65, + 60.0 + ] + }, + { + "name": "高机动-高负载", + "figure_ok": true, + "summary_ok": true, + "table_len": 8, + "summary_head": "### 算例结果解读", + "soc_min": 55.0, + "soc_max": 61.41, + "first_row": [ + 0.0, + 1920.0, + 0.0, + 80.0, + -30.65, + 55.0 + ] + }, + { + "name": "经济巡航-低负载", + "figure_ok": true, + "summary_ok": true, + "table_len": 8, + "summary_head": "### 算例结果解读", + "soc_min": 70.0, + "soc_max": 76.3, + "first_row": [ + 0.0, + 1275.0, + 0.0, + 40.0, + 9.35, + 70.0 + ] + }, + { + "name": "边界输入-自动裁剪", + "figure_ok": true, + "summary_ok": true, + "table_len": 8, + "summary_head": "### 算例结果解读", + "soc_min": 10.0, + "soc_max": 26.51, + "first_row": [ + 0.0, + 2400.0, + 0.0, + 260.0, + -210.65, + 10.0 + ] + } +] \ No newline at end of file diff --git a/data_usage/usage_stats.json b/data_usage/usage_stats.json index 1b02c3f..47c26cf 100644 --- a/data_usage/usage_stats.json +++ b/data_usage/usage_stats.json @@ -1,4 +1,4 @@ { - "total_users": 15, - "last_saved_at": 1760890996.5595384 + "total_users": 22, + "last_saved_at": 1775496284.1673155 } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index e2e56e8..a36ea6f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,23 @@ # requirements.txt -gradio -numpy -control -matplotlib -aiohttp +# ===== 新增:PyTorch CPU 轮子下载源 ===== +--extra-index-url https://download.pytorch.org/whl/cpu + +gradio==4.44.1 +gradio-client==1.3.0 +pydantic==2.10.6 +pydantic-core==2.27.2 +huggingface_hub==0.23.0 +numpy==1.26.4 +control==0.9.4 +matplotlib==3.9.4 +aiohttp==3.13.5 +pillow==10.4.0 +# ===== 新增:混动模型(Model)运行依赖 ===== +torch==2.4.1 +botorch==0.14.0 +gpytorch==1.14 +pyro-ppl==1.9.1 +pandas==2.3.3 +scipy==1.15.3 +scikit-learn==1.7.1 diff --git a/ui_components.py b/ui_components.py index da0af4f..25af302 100644 --- a/ui_components.py +++ b/ui_components.py @@ -200,6 +200,37 @@ def create_root_locus_tab(): """) return ui_dict +def create_case_demo_tab(): + """创建算例演示选项卡的UI组件""" + ui_dict = {} + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
🧪 算例参数设置
") + ui_dict["profile"] = gr.Dropdown( + choices=["起飞-巡航-降落", "高机动阶跃", "经济巡航"], + value="起飞-巡航-降落", + label="工况模板" + ) + ui_dict["sim_time"] = gr.Slider(minimum=20, maximum=180, value=60, step=5, label="仿真时长 (s)") + ui_dict["dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02, label="仿真步长 (s)") + ui_dict["initial_soc"] = gr.Slider(minimum=20, maximum=90, value=60, step=1, label="初始SOC (%)") + ui_dict["initial_engine_power"] = gr.Slider(minimum=20, maximum=250, value=50, step=5, label="初始发动机功率 (kW)") + ui_dict["rpm_scale"] = gr.Slider(minimum=0.6, maximum=1.4, value=1.0, step=0.05, label="目标转速缩放系数") + ui_dict["load_scale"] = gr.Slider(minimum=0.6, maximum=1.4, value=1.0, step=0.05, label="负载转矩缩放系数") + ui_dict["run_button"] = gr.Button("🚀 运行混动算例", variant="primary", elem_classes="primary-btn") + with gr.Group(): + gr.HTML("
📝 结果解读
") + ui_dict["summary"] = gr.Markdown() + with gr.Column(scale=2): + ui_dict["plot"] = gr.Plot(label="混动系统响应图") + ui_dict["table"] = gr.Dataframe( + headers=["时间(s)", "目标转速", "实际转速", "发动机功率(kW)", "电池功率(kW)", "SOC(%)"], + label="关键时刻数据", + interactive=False + ) + return ui_dict + def create_chatbot_tab(): """创建AI问答选项卡的UI组件""" ui_dict = {} -- 2.54.0 From cf9a1a2654cc1dd3b878ee57de3c036be2d6fde3 Mon Sep 17 00:00:00 2001 From: Hongru Date: Tue, 7 Apr 2026 19:23:12 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B7=B7=E7=94=B5?= =?UTF-8?q?=E7=AE=97=E4=BE=8B=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gradio/certificate.pem | 31 + Model/data/engine_gpr_model.pth | Bin 6457 -> 6457 bytes Model/data/engine_nn_proxy.pth | Bin 0 -> 22405 bytes Model/src/distill_gpr_to_nn.py | 275 +++++ Model/src/engine_dynamic_sim.py | 431 ++++---- Model/src/engine_gpr_class.py | 28 +- Model/src/lightweight_model.py | 70 ++ Model/src/motor_sim.py | 542 ++++------ Model/src/mpc_controller.py | 298 ++++++ Model/src/series_hybrid_sim.py | 28 +- app.py | 328 ++++-- assets/knowledge_cards_html.py | 1675 ++++++++++++++++++++++++++++++- case_demo_functions.py | 1098 ++++++++++++++++++-- data_usage/usage_stats.json | 4 +- requirements.txt | 9 +- ui_components.py | 359 ++++++- 16 files changed, 4338 insertions(+), 838 deletions(-) create mode 100644 .gradio/certificate.pem create mode 100644 Model/data/engine_nn_proxy.pth create mode 100644 Model/src/distill_gpr_to_nn.py create mode 100644 Model/src/lightweight_model.py create mode 100644 Model/src/mpc_controller.py diff --git a/.gradio/certificate.pem b/.gradio/certificate.pem new file mode 100644 index 0000000..b85c803 --- /dev/null +++ b/.gradio/certificate.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw +TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh +cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 +WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu +ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY +MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc +h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ +0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U +A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW +T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH +B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC +B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv +KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn +OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn +jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw +qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI +rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV +HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq +hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL +ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ +3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK +NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 +ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur +TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC +jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc +oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq +4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA +mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d +emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= +-----END CERTIFICATE----- diff --git a/Model/data/engine_gpr_model.pth b/Model/data/engine_gpr_model.pth index e9dc524ad3455130ff4dffb5228f7283df25f9ba..e9238e23f5ac0a04133ddb8f2457ba0e5fca5f04 100644 GIT binary patch delta 472 zcmdmKw9{wR*XiBMo0pi=Q6glac$djlKU1r1H&!$$pV}* z%nA$=lYKdq8AT=s@{2BR;9#-6nfGGbuWjY~m!|wnS^i+Ly@0&Zv>g)64zK5Hnk7Z< z*J^S5U3JzDCd}bfu&CVfep_O95z`$TIS&&bD@*N%(*^=;j>B6k5`@ig9 z-=TJW_kNJ7oNP}k0iY@Yuqqo~7h8=i#>SP0B^B1HU~=?f z9We(Thqpgpnn^iq+T-jKHv1abuBlfa--p|^`3~u4=Q5pev)knW>SXvAMa4iK&^Hv7v>5iJ_%| znW>qnA&_HXWNcz#Xl80)X#&zM^73uoKWx4$I+u|Jz=7^h-0Rsbr!DK-``N?;9RAlr2 z%(a?!{^fpEFD>y2hd%8GsS>T6zgPgMN&u|NhS$Y*!xG`;)1>(w*7#f!=yn!xkQ6_<*M9?*25HbBO)`WuOS-RpN`@jsiiC)iLgH>frb?QGqCumIq&XxF_t_^& zhKz-X3{lEFBqH&@JQkN{B4`>lZ4vX3ZvFU#~TR>o)lX zNH6gB^z!%eoUv|Apfq1(yc1tk-&H~gz0G~2*J?k$*qXpi{%d>;-Gc(x`36Y4`+Io= zt@HJC5A^a2*yQiN#Z;OvuCMPZDU=8hKDpZ4i$CaZ5$RdJn^p%p|J^CgAM7eBe8!kB z;VLftzh-j~U(!K5kT12+LBv5kfeSRh~RkF;0> zU;U5th`*&Z0{J7I_?mwy5B`^Ot$&D{@wNXJ*ZG&Y?jP|{e~aq{@<%)I$GFN02kGY( zXlQJ>)oY!%PauEnKQPSsrsi9h*2nV9|u6N`UfSn#L#NX`=( z7Ra~!n~F^&f2xp?kc!pcK)gUc=fsEqq+)9L7Zvgkm??bgzp2>#i^{Y=RBZo7m>$TV z;l!W$pH$5MgUYObU|90)gczZLeEUBbVh%zkv;Sr?=WiT`K>l1O{=ENWV*WRi`Tszf z%3mNv5x!l=zfdCh3;!^2`WwMHkiW=@zc^?Y-^F!^aB79-5xDx#8sRVb%k*6JU4^|O z{H442%lS%9Fodie#;vYq(v@UbxeL%@0~|CN|f`JeVb6HphK@?Qve1P6L;`s4Iw*_!=3djDgZ zeAfjA`g;B05CJdL|G3IO?SJN=B4Y7hIQ+3VBjZ`NrvD!VL_&Ep&CJY*NTQA19~KXu z-sT`ks${CjT^T}hMprR@`wUVzSc(MA<=A&i8%(s)t^|7WVt&W$w6}i%#W`k zsqcE3%y?6_c}639&=fvc5wnxyhv!plEZMy!jW80=Kg2J&6Ph5RbJS!tKl7kj9kTSbgIxdW_gfwx?7wey=$(jC#cGY29UU zDq$?@=PMS!)Q>2w5@)_aPnbxb1WBu#Lf+S3BiFVBu!Y4O-61YQP|O0&(+0tNwRddq zg;T_)cNlYQ=^`p>A!Om+H)Ke+4SA@kzMNSa-p;!Ez7r;){S zFX*`DizF>wi&PCSB4fLX*zgd0c4(EMwOZaj)Zb%Irj6RjX3~r7=Zayqa?^*@_BGG5 z*7-S)`@LPFwqe8Q+7-pN*5&e@Y>;>rOWel5#9Xx2bLCNXRl}|p*6^&Ct{zsq*2$b1 zO&rT5{b(junl81K!-vqAUu#wS)y;tIJb#t!uGg%!q3+h)WTjfg^Yh3% zi5O5LiR%&m@rKlA^xB(mqP*e{I#|C`_aALifU|1$q|O?T1HF&i2;9Yjh0k(|+4 zb0{;{1Cd<@ogRDaiECEeEnFTE7-W%L!^s^#(+vdy0QZz+JAD?f5h>+uehW=? zlw$XXr(l@Tb=c)yj9qf7a0@#iNIoBfUnsMEA0r@dC1TI6lh9T)6P}BF#|0u85bqob zzE2s5d}$J_6x#5%>Z{-z-y;a{lmeq~6R6qSQ~3L15u}Y!#mL+?RGxnsN4`G=TUCnT z#hyB-O{oN>5g*`kmKY1D&cj(jYNTcMTS)43r=NO?;N-G6*sSbHA0`XXqv;a&q@Y9a zs(uj&T!U%uH3?*s^tgIGH8309NxLft!@N7OczN+5jJO+$kL$FFW}}>7>ta99HOa+@ z9WmUc=a)Flub_vADS@cI3c3IAD@-K4=oG=D0lPwYssmaOWY|aJV<+Lh4eE@_XtE*g zgUGcN^YBRLeq6Rtnw|YU2lgy9CbEOgp!rNC^tkkZ^Zim3Sxf~JpAr0Z!kkW$UQl!G z%4R$)Xb0y!QE*DDqVgO3Ig9cP6dkHTG-@b4VrGj6wmERUtwYI?Mk!|6bP`-|WWkI& zWhj@HA$Q+C)1yymBgaMQAe7Vb@>-BBe_nayCsD_P$0He*=P zABD4TbaN{XZp2%`44$Xf3Oweo;l>?E#N{cva8gc;_`e~jwIUg(#g)@@X;*2V+!FYC zIst6F$51oN>v*}X5Z<&cghhHIS!?+yrn}=Qey&wQjm~Hqb@D4!S}F=1b|pBz_bfLh zItOP5OohrPN@UA@MQjsXfV9KUp#Gx@JG@AWO|Ub^&d4~r=KMJHTeTE&v?GC}Yp`6& z&0x1?AE+!I!)^|eA@o=_^mAe)xb{1)UNeT7^76pa|0AT{yaKH=(_wteI7l*=rSgl; zKwNMyl`)=xkLG>nzDMnc?j{vxvVFRMMkmnjr@LsK=5%acsX;E1WMaepzp+SRG8329mB?AM}Lf9(fS^c z5Pt=42E~F$8slnQ`@uI{0t7dB_#p2SmiTO59o`E$_g3PST;;Z_bm5^bpmla5+^!tY6t^g|bz~^hwb_C5C!B^ivy*Vfl_&J>O-4iX z0eX^#W5D+-XkSN&lzI=A9vH;NqA5FZ;s<2b`_slb3S@AaKAyYm5B1-Y@Zua%BE^eB zzac7cr!EPny`qrwIT`wP&gRMP+XgMN%5d0d6o|iC4sHsQKhd_>30smyfM z2f>D6`lv8%KisPQ4YA8hQBv#}mp+gR!^VBXo%IB+besg;Gxiv^Ef`fS-(r`;OsZCM zoxa(Z03D7e(QHyYUa#JR+s-d!DQ1y)<--9eKMbhz?Ia9qYlFcDYGI1#7mSlr69kwq z$2X^SKxbS64tHW0T`o_)Rz}j&Kz+hl`{GuIIt;G2M%#Pi$)2m8Ab!`KaV}PvyKXhU zopT)2qz!5BfC^O2o=-P-)Zo32XEh1-vpV7cTm$Y}e_U76+!{d;0~%A#qo-qJ@f zYljxnlDW9RRga6Z&ZF0iG6Zfnv@o{L1b@GI2d&)|aLD#4_PzTEnbP@SHq!w2F8nG` znI}sYxBkGmou>r5mz@WA`W_!!k7hDmui$5c0t^isfmL6XNs8nw?)bg$z?;*DVf-Z2 zQ1%2*d2=kwi{onLCCJkEGvJkVwqTo5E7BWl&^dn{O5}RLrDxaag_APebme=Tp9TZ_ zgFdj}+Kyc^YqD34hMb7sDcD`;h`(~(VRr3a%;^$`xfi`L!|XXt75az8$KTLrk< z=>R`_!UbjVk9l|N9>J0?s_2$5n~Og)8$NB{2eGD2P<}pwO&%?dKSrK|tw(=x_xmJa zrs**Vy6}j)9TUf{a5D*Tf(swak8{wO5 z1Pt+sgNyl+s9CifR(x{9x+TLgYQ$MiGDDoLJ)FdqR$QRN8-4Jkkr`{1QiqKj&cR5t zT+B$)=C%$y0J>XeU{T9zc<53~6V_&7(fkAOF{KoAw**k{U$xa!RL-OFMNew#wG@)i zsbK%7EY5s(942OlW7oWKP&rtOo&1#vTd5M+?UIcOpDJNr)j_=O!J(C*2%Zp+rBCI; z@nFV!?nUn-Ztu;zkgeW|+KQq0>%s&0?Ip*;+COol`n90eLW#BAkL8{fmvZ)Z?qTCX zO$al7imiKQL-&{>3_fIpf{lblY9B|_L0j=>s0EyNXn-&xMn>+~k1=M?Ac2<%=D**< zvDQ0i8}N#TUOvYi{C0yTo{EN1ncX#!#{%%CVGgaEb^xx34rb4nje{4y58>2GX;MD* z1X^x;!%a?aUI||ItdkqJfl@BGsu(XDI`>VFnKmZg}5Ztz^KJm@Hyr>77D`1fp?iK z^fke&t$We^`4b!xS_^t7)!977RGxFI1eusJ7BTJ~<-M4QI+^=0|7|P&Fu6@-+y{Uk zJ%BTZpQQnO70Ax|#VMXjMw6aw;+`>qY&`p(R&Q(pwUF^dZtGI`%!aaeb*H%EO{U=Q z{D?YTo=CK6@?UfwOpi_Z#%O<+B`7A}#^8+K zRA*`v4&L&VPEZ{~T%4P@%MRYO`lb)e^d1i4pFYs9VjU1W^bu(FIPtFZhv4wmPcTgO zC>MNQ5pqcf9n~UFLRLjueZI8-&37ljhP8~QOo~CnNqz9Jz8Xb}<#5uJ>vZTBZO-D$ zemFUO0rYFm0*mwls+PYFq`o`BqtV~Fb>{oP^GV3m!R{Gwx>SKZj{OA&6K3Em+3_&- z^#?fScmOs9C(!lf^1S$@da!!bitUZ7>7tVb5E32?@5)Wt+^h0v@xEOk;k_O5l^;^? z{m1a~S8ozHRF+*h849gl-C(yezsAIJ1UqMT9u6JIhgVLzT+mu~YLdf;0ry?h*p{O1 z=B4bst&Y`Pl{}vH9eGTcJQX~`@6w&F&Yb&&7*v>&46)ly(4*W4U-VqWigOdF;=61( z|62;j$i2euPp_b)ZxT%0tVZnmF2aUO$MN&J8{Dd}mAtCe8*x%7N3W*77Wh~{MrDyI zG!VT+haTL^B|bgG9T_Z7Z^)+Oq?QUysu|3pHw>nq+SC~@s}@9iLBXn zDbD&$z3JTYb8t$p1|OW0!7;Zh@%+P~?2T{@h|gb&78fnL?5XnYDsNBKgE#8?Qc z*QYI6QDpk#sl-z}4h-%Mx0k#a%utb7VTs?uQ?YV?MC<=)`8eNSLe3#Av0_7jh;1CaJOnF*FwpjU=976AiA7+6m4lwGrI(u!R%&v+phREF#WO>?HVjQI_ zXgwVZ15^QKi8*0fk0?ns9n3^UN0E;Q4xvP!26ovuL%-x)!LZ`-6|RPGth;z?3bfr$1^;7IT6npjAXKwlZkb*Hfwf?=RRAQfo!}L zp4SyYv$)=>nbj7=>bnA0V10o1D#=*Dd-j%lJwcYJMktX(hnIn6sXQCK+yx|cM`LHb z2;4U^Mx*`nVL|>WS{-s1-Ew>|DB}VyG1!OKJ}^M3vg$BSvPhU1FWFSln-9lu!v7FHgofz?KW3DaEh(fS%l(NZNL zT|IdAbSTFnT4DSxOPqw4d7{m^P`e@p6zgo@M!Ou!+;gWRBzL0vs4}`^QX>SGz2s(n ztf0-0D(Lkq=~!%X6`Q(8z~=kbCFrs%t9GJ} zo*1k%DYwcYQK0YZjd$M^qq15eEFXFlC37pWef}w&d`lD-lovqRb~|YN(2A`cr@?7P z9!fTRf!|hpuq$d3EAMoMx5n1ObN4bP8lXtDWGCV~nIe3;tsX{kIao4K0ipI@oNQ?% z#2kH!Lrl`)lH@nq{PYg470iK62d9(ef(@W>xRh(`Yl9KX03hkkS(T+ zw;M9K$;L{gwa1Z-S(8s^EP4sgUajWx9vhPFd>we$FNxBi26`*(2+LAM#ne;SILMo% zxawfa@L!zd!E12yRyPzUij##~Te0pja#x4Ehufo%(~iL>xLM|NSneogR`uSN+=|j+ z)30XY_nv6<3A?~uIVMS>Z9alm@BkXl@uG$9N6{@n9sF7>iMDhaO$s^-ClfAnkCYBV ze6uo{ns$tPBlQ_{9D3<1zjgR&g(SHgr2sKQ@?fe-D9$h04xdAnh|SkQtSkQ)wLE;6 z9yfHt6B(MQvi~W4QfEYd&zB%da+P$`vnZToTo1?cA8>Z7=HOvDd%7moh`C;=;x?q_ z;q-AEK|&%BZ*_EY{YNrrL6|mm_)&$YY+CSr<`8B&SeJ^w*@Z3px?oSNJ5HJqftRI@ z(lg(jfgkjnJD@uQXFOJ7X+Lc7(!6+74QatT!(w!fn+c6(I#fe{8Y)d(4LRA4kbgQH zCfry8`)^#KE7GrFqI@bOoNdCp^@BiTr!*P2f4N}yYXsYK)6npwH%hFK2FEgGNd2)F zT8Bkp&7@<{v41ffHcSBVKpAF#Zx$^6FbH?e7zVx?8B>RwCgPkEicsR9$h_(@@$$4b z6y1J_xAm0^+~AI&Y|cd}a3q+tF%)ZZ-C#-KB6u_-6TT!*70mdk#l9a%#!izuXh1(M zXrwLpH%g(U!o?~VojiOpc?{^~n?c;-ba>sQ!yTKUM@!ieSmJ#cMr%G5Z0lVFUQ>_2 z^Yd-k;TT{w3GJ{T=^8{R?!_4rU6|LY%w1ga7*x&1)6coG?9n|tVwX~hbBm9{xOiW< zrq@JYQgu?WZz$~cJP0jMrKrk*qww(~!q^Q3yceegR2X^jkck+}(pZl<{$KEE!ECs? zqaGzU0D!JI(O>ldO4jCLUwHX5F9RM@u(vGx-7q z3!K1HwE$eNDblVFQL&m>^( zRYhpc`N?haxr-zCqGa=zgD}eJ6TI53K_^d+#h|535b}H@C=6}Jw+=dZz$F5A9KC{9 zrp(1jNs0Kd;~Cv7kqe`5js%TYS^}HJ9q`>#hB@b?Leei&$m#b%>qTuKu8{~6>qf#U z^H%EX_!vt2rAU6b53ZEi54|I%pcZF~a$&Q`be~3mco$G|YKL3zwsAY=Eyu6+*QwN! zqp<6DJNyzpFBY67(7xEg-M(}ad*fCM&eyrn1#~O;4n4{_KFoqam+s;o+br~0gLKpH z7?}GroyJ7-(9L-$+nzj#?UZdq1EHN?ow@-{2WL^I#p~FVS=x~HqlKHPBE=+HzQdC! zM;r){fs_MtIE!;X@R>;=td}1F=2L@sBBSjgYH=h@PgmjQY|KX&h=oNJ@;u%6srZog z;P>)lG~GT*Fd^zRRpx)isGtTY;cjwQ6=G|KjW&at?I!T7zm2+NlyOgL>$#!oGDO}o zA8d|a#|$kqY@PIuJEC2Q$CY|`Uq_AQnyho^fY=aX)!E5ibnFm_s~SStq&!+TA_Wr+ z=7U)B4{l6)0}7Tmqe!b5nPeF-G{i*SaPJ@C83 z0SZeq@zJ^KT$I*MOpH8%_LzVl#uQ-Q{ViZzkp?m&WyyS_0+g>X5*E^1CgDE1cYVN|t;5;IMrE?|`U6M{&!OwmYT-cJPc+T7hJD`CxFb?2*t>fk{jxud zUJ;chyyO#*8!?2u8R$o4=N#0ZSIU*1Rv?+TIjtK+l@M1_6T)gmD;2&}m zio;*w_JqT@Qu-r?ZXN_rJ{sdV=>&BA@E!O)VnDv_r(;$3Avfy^u9Y()>vzgB4OM%% zXf*;;cBSKZw~1)wnTKokMDZ>xJOCd?tfyWJ9%I~`Ht-)i85|o&a9+3eb1w`fSY*m# zI()qZqndH}D_CedNgu#2J`B?zjUir+N-!%wnTmb1pj5IPy`lo3*laL)Cl>E|K#I z=V9-Q!4RtQk*fF41*hB}5FD}{&v(0GUtKOYY0EV$np z`!O}3i(-8ko?ax(nepautl|=s-VK61FWO+BIM>R*E*r9SCo0Da2F>^KCDg_j9D0p_WPGZ_4|DEzkZayuQ&pgS(o8kQXQ3aoQoY(s(4GCN3)wV zOKDZMAJ?0;1<#dU!O_p;;O1otzw!^^M)S3pyZA9APEG@ZLaUmC^KwA>=QM08AS_w^ z4kpekqFeH}bL|#`i271(a6v^D)oe@*yV|JV>#HzN^e7IyQ3g+)OE`}Ug*f!=C9Kbw zj9VV2qT9ex-ls$65biBTd={<8N1yLwNZvML>VKJQkCg;B{d3?ur3ds=(&2g104T(} zk>{`8av3_wf_3XIaN@5Q^A?7Q;HKvjA*kjDzW6wlHWY0`uZRlZJhwvVpfJ39#tC}r zhqH|O+?vwTI!@Py$1|25#FkBe!ApUBjI~T-nJ;otxporV?FynbXV*~2x)3No;)Z(K zN3cv`5c_;r1Laq4#n+dP;j|-Tna(O7Uieo@>Lek~>@Vnmb3__9?EPp|6toD&Xh(A& zn+s@Xg9SX_x*w$5jd)8M_wzolTMS1a5g+Bdampix;1#2H5c5lpNj+Q$rS_`iYp6Gj zsOypgVMD8Z_EUvWqBUMlXq59~ZLU_d+#f4wZhpm4@ru!-gE?yjSG&0|si zv7Ny9LOrg(BY@$~#{}hO&*80L8l97$f~U1VL*mS4n4_UeuI~L#w|_E4l^G{+>l1l& zU}?Bk4es=2wwj0B#Lue9BudU!Lk_tHe7d}w< zqX1QZ4r0XzMUdw|1m&C7;=c7xbm-x^*p+?%@$U8?Newgp_mj08+roWeFPnn|A2>7wa4EB4$dmR4SsB}P8wf~bxuY+z*~1k`WA z_y;~DC14@}nKevmlrXoE7)!OxyI^CNDd#*?lZ|qk%x>?WLnd1U5+x|at>R6Xdj1Pu zS2@hiOIfgMw^~5fS&J%BpO&qiOtw$6N2mQ(%*OO9O8e{4>6==> zLHRlw_w>L-)fw#5XbSnuuYtjkK4CoppZNDxGqPnp6DV4+EVVp%`}8)pzTu$i z97C)29fm`3#w5PKiB7+{18+pX1Do>+^mJJXF6Ud)Z)eUx6g>j@vrGhEW)B19vH{rM z5)12+w&1(w8!*>+4JAi(aLoOycrn_vX2y1RQ05eA=Z!V=`?8JrIC729Mu5ag91z&p z4#$HVW5FKs;rnZfp*=1&_6j@UV90JT6W*JwTaW|4TISQ6^SkJ=#&Mu3r;ien?vz-uX%Ql-J_2Ct;Add0} zQpemfp0`R2EVkVbk4~pS!nXZ5qdFW+AFjd4i}%1Cdk;|9l*p~}6bSmxXrNn_8(toC z7ALC3Lh{RfFzeb87+LiI?j3A~ggG8?Yw0+uq&VS8S4~t(bZUjW6u+f%zP!(Lo~#o4dSg7TB4s#>YsTc^BZ<$vDCm|s3oE2ga0#S@ zOMjkE!>)|woldTX+Kp}a)kFYKKi&cP8a*ccMbsCsG)V6KGOcW z8SEhyVc9o_kgqM4Y<_?NS_s^*W`s4fSQ|qfN5}It@7>2;af6xSn!%uYzzu7I)R_@U z1If=jxR{g!c=3xi3Y4D0(3eG6vS%Fqa%L%SdU_UY&+Fof7R%#kPjPVaI74sTbOAc? zJSUPfp6P6?qAN4op?%l@p3RA*2S05BnZ`z`Vhc;-I^DyJeE6!th6t3SJ zjb6`QtT6Y43WjAzOm&c6x-zN|F&{w8^nG)zEiI5d}u-=zU%mei|i$j72WJ zIXxK1TD79y87X#siv=1LZ{W!a@4j|6SkSfP1j_!rjZNlL;eyRF?&Zu8L@|9BQxu-H zbUztjsMjpC4~xSj;U2?WM~v=Xd;pveF2IT>4>0VJ790D*mGgVM8fL9^w=7qFz#Xox zfHmlerJYd%YpxRV#?HgnOAspp_JH))hv>}LLG|h$$gevIM!Pp~;w#fRo0%VK@Psu= zNBH3qMP>5d?i^g7UVx!WVnh_qa$C$i;n!_r?8wU(w2Vhk?iYh3Fr?NJv6vdX6qdYR zk9t*U*ec9Ve7jb`?O0LEjn?0SrCl*R70be!eLJ^<+28{(@8)^B@TnZQWr*W3zwuP! zb`q-k96@KpQur)$8J%iIF#a>)nO(L_U^hsUD)?)2tUHx{IcP!R@I4pxUYD%2R$<30 z?(jAiDv`2DE9e6z&pf$WsBbe|NLmo^gkgy!@5Gecrk`$DY4D|hA`b*p2TV| z#hpn8Y>v-ZP!S9g)^RN0!q!d!ZtN2M-`+@4(3` zKA=N?7T6vyp-Gi7uu483jz??2&g(`bzQ&DvDWb!6>=I)dO(~e0T!yQ^RlqE*P8jcL z0+HGg-1J;^oT|JQ)yIw@<3lA$`>kqFZ4HLmcVh)L(|lo<)?n6>mJ1mn7wO&K;S^34 z;dHwNXrLnk9}e+|eyN`nfj-0ywyC5GnYvi%uB43kKKD z7KE68f?bop(%?gnLF8pIjNf?=-}Nc7w$aOQqSScI{lTXqGP=-zgO3d(@?q^zU;5Tb zAEsT|jqU~S(bF#v?dQmlt*0|_%hK(5J8mdij4eR3&@tSiatQ4Y%1cqlM)1eOLDMvog;Q96ky~7 z0d3W3p_h}j@#ywfXcyfAAyuntPQKg$Ed#6I#I*?S?3t6W)`vlARvJo7&wi{0vOr|- z&WvZrL-3{??jSaSyR|&Fx~h{?^NR6D)MQNR;E*fu#(1@x0!PDPWZCXA*dvU?PoICH z^Bk|^o2N%$@lQ=6^DYIqzm%bR9ktM~+KlZS^MnQke89T=a4xThaZiq`V{5fLH@YJk zFIgLshL}<0^sjZ~aJWD9dS=YFe$NNHzG2MUU>=oAT8}}ZBQPv}yo*wqm zhT%~bYSE8y(UKDLXvyllEK~ z_TG#f?Og!ly6=Fh$8xaceZkMyKY;zuNiag>0^HOXjxWpmupvj08N7P|W%Iwn(Kb;M zur?20694%{}1j&2 zU%>dESrGIq4Uag~(K$zQaC72IdgAv)mO61cjAG&Rjn-D&x+MpbQsQt}zB6=<`Ai>~ z^4JU6L>y~e49^zc=8Qf3VO;xi%z5?@GJa>*#7mh7_m3umOQSxZLegGr3(2MxXFV7;z|HXPc4jdv@->U9+yw|xR($7kY$B`={n-vH03 zTtcJXP)=C)Ol2M`khwElsa(G?e9|xHJ{XK8r<;6u^`BYIsS|40EB_D;EeA23+e$Ft zIZ9abpvHJLE!1JLr=aRf3NL7L20q&L1a`me12g;O@KbU-3|WE5j+Jn)B;{aqejaFA zRtSD69E6`uPare<7frAW6qt?d<806RkjG>ud!`-*ZX+w;UT;4*X-Ba;V-%Rm^LC`~ zzhZagbv&o^1UQdYXl>ibdiD>8g7jhJ!nZ!2orx-2yj-93K{V5Sxs*7*=M&W(k!+oy z5j(X;0dcA4Q5T8>=QOaP^p4^&y zu*s!M?#v>^KZlb%V}f(TMVdrMFi1HMuLi9Wd^_?Os%8e#iwjG5x=oHC7QFz@ zd${75$J%UmkUFZn9l_Cw(s)bZGdF#cEN0g(VgGV672)Oyo)GN440_$eSXGcQ8m9l? zeO;={x^rvbWcDH4?D?H*{dNJp`=-&CkH(R$>K-87s!I1BG$pqh&EWf#eK>4lCK}DX z!-=J8;F4c?kbeCTeLw9pjqq#WJx~mXqw;p7vcims8jqoi3Trs+x*2$N_f&YfZmO_m zsE2piUwC(U#u2W5x=Lq%Z@|tMh^2O0wwaVnc?uykcyF;%~@^W+LPih%#-g=j7Xt;%ajW@AT)Q(2J^rR=RPsR7sonh~fYiOS%tV?fvk3&nM zssHqmxUqZ#ems9<56!+AWVwtfh|2< zT#WQ1*r$_(ALE`|g36YrI_ULKjujvDx;M~abIe)Nr8{&WEtMEfSE3EhQ7q*3 zPZDeKif8Te5?WO=$m_E^1WOuyp+Z-NG>R#Md%}5qImiXBX&)ElkNQoQDpqq!J~wIK z@h1@2atz~ErJzz*7#OXpzy|xT0>N2ZZXkOZPG6WqTi6~LG;TMhrrW|>*Kx2ket&qn0k8$!w(8c;$<4INa(NkfSW z{qRwh)OY5hifab!+II-Xt(W0kgkSw>n(PHf(;^HXyANdcD$BDKTE79iBbpd=ZU?QajK+rPJK=?@242YM73f*z zV9$FMrY5XMa6Wzk*BA72U2;`mIrBL*`#iTgRwB!mF4ZTGN0{O}g$ZEi|H&$iMnlP# z7<}`f2)x#n;E6HU;QJvbd{JA8l9^R-YlyIxt@+2q2LnR<}rT?8JMIk>hw zfEU%;#`!ld#emRC;CGtf==)LN_3;B7>5K$b&84_v0+;a zK3BMn#p-8J^ScP!)uhkDv#i*X*CnU|#<0mI7PZQ2;S~Q2I6s^Zlb1-K#fhsZA*0HS zwtk{6j)%eXf&w!L)+Sx9O3dOcAMfn$;ne)%u+>Ey7V96!$v%lZmlf9N`*|b_%&t9~ zaIF)0XM^G6q=i&#|2cSiVlu?1C4jeb7{p$C#KlaTL54jRp4B4iu+}6K$z^_(VJou1(CsX-xtuwRISq z-;+h=EIW<6#5-YYmN@w-%*B5FiJ;}C07gMn&^&W9Bp&OAk8h*6vm;(`54}sE=BpXy zjhM@go$3qo^y;D8?gX4#<_*K`&wz_Wd3E)|T)Ja<2cC+5h}t4`v{_iYVl2GFxo7MI z(#@A~czL_!9QisJp`Q(7T9sH|oEy`)B*|ocC4+CtENI9T=Iw&!f$9SToR_+l=B}l@ zJ$h1X+1Xlf^cW2(s=aj2mv&mYT#|`^8TaAsD%4bK21Tn5!HH=#kht3q`zJ)f7SN;D z@2m%v)6WFEGNa&&&3tlw(GYe_TaDE$-OJmUlS?nh$-4Jg&jw;VpIV{s-mn|e?uWvREeD~3#zDcRQS4&ONRl>CR)kXHHHf=l-?u=+X2=*$ej*_?Zerg>pY%aNd#T`VIyumK9t-Q)}qhX zwS}n8JM>!dQ?P(AoKy0-Ci8SMZ*sK~k@p`5lUyFbcI7fmn|}!=kMn}oCpYMi;qBD% z+i7~)U!I(milYbZQ#qrbQsmacL!h&82O5~q#4U4Y4{K6jOVBlRJ=aau z4qb#~VeQlGs84u1SB7;YW@E?9JP7F+M!YvkvE~`ap}#pE+I~0V_op{F5%VeN+Z4@B zjLyQ`yzAWS!&1WUf+M-5=PR*oS|;5yM}~!~jv$hXnJ{BrICf4S%+mEvVeOlPu-!%z z%*URm>q8Q`VEb-3-Mw9yEAOCpb_RpxM!or;9iSoXh+iGaOs_RYpXHZkp~ZNbnZ>^&xbT_)7yES8v$;q@HdjS6`| zly^Y6AXp^|wVZa*nu@8gcycH8qPZ2)zZ%ZCm1;3+a$&I9KMOV;n2R|&;qaT49i%9n zr=Dubg3IkNEPrK)QJZJev;B<7k|nJeyBEVagkM`!@(rPWq^ogE}SYf3M?d!`Z)6HXqOFM@0cG(${$j-Cqo1aZz?`uL%4u{zr z7L7IEb>T+u9k}qGA$IlaVbHox>=>!Qs9v~3J(6W>-WmlserhqS&kCa*7oMaxnH|FB zHwfETDw2q%t2iO{70>fKJuF>g4%?;%LEX$_c>ew^+~T9poUwiYcVDi85z)usSyqj3 z-M)MXXKOd*Wx1fL7exnlC^2^58^WHH5$ONKA5$FO(IJNx(b3&;@Jii}8a6YX7gm`G z@uzd~#>#TsH?jadnPPhNpa_`Iu8HAePt&*jN6~`*UTSM_FJ_R+iFxu{r0`W^66=~NTwPtm5!ty8&}YV@2a65zQdHX z*-*k^Yp&+BqPpriuvB`E^P0H0JmCbC#@54|1`nD)=MBzSEk~^QCNP2}A_F{l1zQ8D>NHi=kwD+#%9AEg6g<7&f*P^%{Wq^4r7Js z5P9_)@^+{&9O@|Zt05u(F4`x|^~;2eY;4^>xf$ON`+ z>v1P7vhQ>qC>&72$>}akuj2t`oIYEJ^hz1Gmc}4QV+(mDr$7=l%W<8I1zH~)%Pi^{ z53X_D*jC|2lKOQ7{J1K-p!*nitav2k{ZdGNUb>qY2FJqLSS3d1xB_$5E`-c~oCymH zXF^*@D%SG7nWPqVrocG~++9`h(F7l;ncfWZqPlV6@jU8Os0OaRQY_pwF@~CJ+>Nz) ze3+wEj3X!aBiGCuhF)=D*33<%^qOw-!X_?&qo)i&Xgi6DVejn*cT=?7+9C)pn-0}a zoT!io=HR2n=Gvcrf;pG1Np}4pVidZ=T_NTMEPlhbX)$_F6?Vi@;f~&9R=ESQJA4It zEGmFIDAWBwZ7CS9aRzlyHPW*vl9{#98mC5IM8WcB*f7JDs9OGv|FTmjXPyir@ANpK4$Iml58zj)!QQ>ntX8f9?XRDs}=fGT-c;YM;L>NMZ|;2&6@Xc8hbj`csV z{on8paKFSqu=4si3`$-J_MRF<=fE3Cu=$0m+}T3AWk%sQcaPHT9{VZJA6l7`)Yt(`!VP} z(}h-P8pJq98{+FWu(`JdU4J-2=UaGSni-;C(@>(zYs4R@Tu`d0BJymVV^GT;^k`Hj zGKLC_?a3PWr>6LJW|Jui^3o=~dWuX~ zT@viE^kC$M9>(bb`q0Lm3psP%K%=L#JEy;geJ-0p$FBPkGcH96Y|_lgun-0}cjUlM zpE_{(#*4Cj-h>B)GR$ik590g#6_|c%lJNd;_PJy7D=MCQ0!}+^g`C28Jg8j_$Is-` zX{~(JD>Wk**^*~@KsdEJzE1EcwCN*TA0N1pPK-xlMIOYizuqKe+kyc zG*h#_Pp3mi$T2U^dE?FCWTpkM>09`{Eq|YHXITvWn_Z?8i-V<7S zs1+O7wkD59yD+|{KY{l74yt0aD)YQ3m)2kV6a#mefqGdYMJ6bt)A5&7M!h!4E;A&F zceO~7Fcy;ZdE|N2JKz)@$J?9cqk{QXSg(2y&Nn`$1J-50S6}&I)y6zD*Ia@tgb7q; zcOJ-|9FHRZ$i~H1mp-V!SpC=EIR4$fWdF-jP9zmg28<{U(}KRX|L%X%mT~;VL-Wza zX7~S!XdG2VORh)@nIm$aiTZ)JfQ5|pCrcjvK|U6-G<+FrVB4@*WPx0?EZEz_;(rXp z-hs7`xw$zyyV#F$bROg4>ckE?ING~7v7^ph7e`koH+IBT6s1jT_6|!nLQy|+imWKu zkLF5opPi2A3yE$ZkEJw4TtsIqL6B!HnbH&~6P`3oTq=TonG*4U*rz^o5 z!ioc>gZn6^lg5Y}-iAwHQa*$E!!0Mx5Vy2RG7MOKopdN47_8_|@)r}gtw}Nt4a9Je zW{4ZpBpEuaL-ez8I7&0b?N5@7{R1&rXQ1S|h#QO~8JcPX#NjN>5WfnMWb9^zc%NMt z7iot0nN*UYIc)$2SDGQ74J8@7rw_njk50+$A)d)38JfNWFx;dW;yFoMVn5yg8OEO( zP#PkhGAtw@LuP#%$mf&U%qfbBwtvoR#2NLGqWS00U!Tj_qgvGQS+b~O=M#x+WlZOZ V-v6>&Dv11!GMem5H2ojb{s)RAB|!iH literal 0 HcmV?d00001 diff --git a/Model/src/distill_gpr_to_nn.py b/Model/src/distill_gpr_to_nn.py new file mode 100644 index 0000000..1e04202 --- /dev/null +++ b/Model/src/distill_gpr_to_nn.py @@ -0,0 +1,275 @@ +""" +GPR → NN 知识蒸馏脚本 +将训练好的高斯过程回归 (GPR) 代理模型的知识蒸馏到轻量级神经网络 (MLP) 中。 + +支持两种模式: + 1. 直接从 CSV 原始数据训练 NN (快速模式, 无需 GPR 依赖) + 2. 从 GPR 教师模型蒸馏 (完整模式, 需 botorch/gpytorch) + +用法: + python distill_gpr_to_nn.py # 从Model目录运行 + python Model/src/distill_gpr_to_nn.py # 从项目根目录运行 + +也可在代码中调用: + from distill_gpr_to_nn import distill, distill_from_csv + result = distill_from_csv(csv_path, nn_save_path, ...) + result = distill(gpr_csv_path, gpr_pth_path, nn_save_path, ...) +""" + +import os +import sys +import csv as csv_mod +import numpy as np +import torch +import torch.nn as nn + +# 确保 src 目录在 path 中 +_this_dir = os.path.dirname(os.path.abspath(__file__)) +if _this_dir not in sys.path: + sys.path.insert(0, _this_dir) + +from lightweight_model import EngineNNProxy + + +# ============================================================ +# 工具函数: 手写 StandardScaler (避免 sklearn import 导致的 +# numpy/scipy 递归问题) +# ============================================================ +class SimpleScaler: + """轻量 Z-Score 归一化, 兼容 EngineNNProxy.set_normalization_params""" + def __init__(self): + self.mean_ = None + self.scale_ = None + + def fit(self, X): + X = np.asarray(X, dtype=np.float64) + self.mean_ = X.mean(axis=0) + self.scale_ = X.std(axis=0) + self.scale_[self.scale_ < 1e-12] = 1.0 + return self + + def transform(self, X): + return (np.asarray(X, dtype=np.float64) - self.mean_) / self.scale_ + + def fit_transform(self, X): + self.fit(X) + return self.transform(X) + + def inverse_transform(self, X): + return np.asarray(X, dtype=np.float64) * self.scale_ + self.mean_ + + +# ============================================================ +# 模式一: 直接从 CSV 训练 (快速, 无需 sklearn/botorch) +# ============================================================ +def distill_from_csv(csv_path, nn_save_path, + epochs=3000, lr=1e-3, batch_size=256, + hidden_size=64, verbose=True, progress_callback=None): + """ + 直接从 CSV 原始发动机数据训练 NN 代理模型。 + + Returns + ------- + dict with keys: loss_history, nn_model, X_train, Y_train, Y_nn, + scaler_X, scaler_Y, rel_error_fuel, rel_error_power + """ + # 1. 读取 CSV + if verbose: + print(f"-> Loading CSV: {csv_path}") + with open(csv_path, 'r', encoding='utf-8') as f: + reader = csv_mod.reader(f) + header = next(reader) + rows = [r for r in reader] + + col_idx = {name: i for i, name in enumerate(header)} + data = np.array([[float(x) for x in r] for r in rows], dtype=np.float64) + + X_cols = ['Altitude_m', 'Mach', 'RPM'] + Y_cols = ['WF_kg_h', 'Power_kW'] + X_raw = data[:, [col_idx[c] for c in X_cols]] + Y_raw = data[:, [col_idx[c] for c in Y_cols]] + + # 过滤零/非物理值 + valid = (Y_raw[:, 0] > 0.5) & (Y_raw[:, 1] > 0.5) & (X_raw[:, 2] > 500) + X_phys = X_raw[valid].astype(np.float32) + Y_phys = Y_raw[valid].astype(np.float32) + if verbose: + print(f"-> Valid samples: {len(X_phys)} / {len(data)}") + + # 2. 归一化 + scaler_X = SimpleScaler() + scaler_X.fit(X_phys) + + Y_log = np.log1p(Y_phys.astype(np.float64)) + scaler_Y = SimpleScaler() + scaler_Y.fit(Y_log) + + return _train_nn(X_phys, Y_phys, scaler_X, scaler_Y, nn_save_path, + epochs, lr, batch_size, hidden_size, verbose, + progress_callback=progress_callback) + + +# ============================================================ +# 模式二: 从 GPR 教师模型蒸馏 (需 botorch/gpytorch/sklearn) +# ============================================================ +def _generate_teacher_data(gpr_csv_path, gpr_pth_path, + n_altitude=25, n_mach=6, n_rpm=30): + """ + 加载 GPR 教师模型,在输入空间的密集网格上生成标注数据。 + """ + from engine_gpr_class import EngineGPRModel + + gpr = EngineGPRModel(csv_path=gpr_csv_path) + success = gpr.load_model(pth_path=gpr_pth_path) + if not success: + raise RuntimeError(f"Failed to load GPR model from {gpr_pth_path}") + + df = gpr.df + H_range = np.linspace(df['Altitude_m'].min(), df['Altitude_m'].max(), n_altitude) + Ma_range = np.linspace(df['Mach'].min(), df['Mach'].max(), n_mach) + RPM_range = np.linspace(max(df['RPM'].min(), 1000), df['RPM'].max(), n_rpm) + + H, Ma, RPM = np.meshgrid(H_range, Ma_range, RPM_range, indexing='ij') + X_grid = np.column_stack([H.ravel(), Ma.ravel(), RPM.ravel()]) + + print(f"-> Querying GPR teacher on {len(X_grid)} grid points ...") + Y_pred, _ = gpr.predict(X_grid) # [WF_kg_h, Power_kW] + + valid = (Y_pred[:, 0] > 0.5) & (Y_pred[:, 1] > 0.5) + X_valid = X_grid[valid].astype(np.float32) + Y_valid = Y_pred[valid].astype(np.float32) + print(f"-> Valid samples: {len(X_valid)} / {len(X_grid)}") + + scaler_X = SimpleScaler() + scaler_X.fit(X_valid) + + Y_log = np.log1p(Y_valid.astype(np.float64)) + scaler_Y = SimpleScaler() + scaler_Y.fit(Y_log) + + return X_valid, Y_valid, scaler_X, scaler_Y + + +def distill(gpr_csv_path, gpr_pth_path, nn_save_path, + n_altitude=25, n_mach=6, n_rpm=30, + epochs=3000, lr=1e-3, batch_size=512, + hidden_size=64, verbose=True): + """ + 从 GPR 教师模型蒸馏到 NN。需安装 botorch / gpytorch。 + """ + X_phys, Y_phys, scaler_X, scaler_Y = _generate_teacher_data( + gpr_csv_path, gpr_pth_path, n_altitude, n_mach, n_rpm + ) + return _train_nn(X_phys, Y_phys, scaler_X, scaler_Y, nn_save_path, + epochs, lr, batch_size, hidden_size, verbose) + + +# ============================================================ +# 通用 NN 训练核心 +# ============================================================ +def _train_nn(X_phys, Y_phys, scaler_X, scaler_Y, nn_save_path, + epochs=3000, lr=1e-3, batch_size=256, + hidden_size=64, verbose=True, progress_callback=None): + """训练 NN 并保存模型。""" + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + if verbose: + print(f"-> Training device: {device}") + + # 创建模型并移至设备 + nn_model = EngineNNProxy(hidden_size=hidden_size) + nn_model.set_normalization_params(scaler_X, scaler_Y) + nn_model = nn_model.to(device) + + # 准备归一化数据 + X_norm = scaler_X.transform(X_phys).astype(np.float32) + Y_log = np.log1p(Y_phys.astype(np.float64)) + Y_norm = scaler_Y.transform(Y_log).astype(np.float32) + + X_t = torch.tensor(X_norm, dtype=torch.float32) + Y_t = torch.tensor(Y_norm, dtype=torch.float32) + + dataset = torch.utils.data.TensorDataset(X_t, Y_t) + loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True) + + # 训练 + optimizer = torch.optim.Adam(nn_model.net.parameters(), lr=lr) + scheduler = torch.optim.lr_scheduler.CosineAnnealingLR( + optimizer, T_max=epochs, eta_min=lr * 0.01) + loss_fn = nn.MSELoss() + + loss_history = [] + nn_model.train() + + n_params = sum(p.numel() for p in nn_model.parameters()) + if verbose: + print(f"-> Training NN ({hidden_size}x{hidden_size}, {n_params} params) " + f"for {epochs} epochs on {len(X_phys)} samples ...") + + for epoch in range(epochs): + epoch_loss = 0.0 + n_batches = 0 + for xb, yb in loader: + xb, yb = xb.to(device), yb.to(device) + pred = nn_model.net(xb) + loss = loss_fn(pred, yb) + optimizer.zero_grad() + loss.backward() + optimizer.step() + epoch_loss += loss.item() + n_batches += 1 + + scheduler.step() + avg_loss = epoch_loss / max(n_batches, 1) + loss_history.append(avg_loss) + + if verbose and (epoch + 1) % 500 == 0: + print(f" Epoch {epoch+1:>5d}/{epochs} Loss: {avg_loss:.6f}") + if progress_callback is not None and (epoch + 1) % 50 == 0: + progress_callback(epoch + 1, epochs, avg_loss) + + nn_model.eval() + + # 验证精度(在 CPU 上做推理,避免显存占用) + nn_model_cpu = nn_model.cpu() + with torch.no_grad(): + X_phys_t = torch.tensor(X_phys, dtype=torch.float32) + Y_nn = nn_model_cpu(X_phys_t).numpy() + + rel_err_fuel = np.mean( + np.abs(Y_nn[:, 0] - Y_phys[:, 0]) / np.maximum(Y_phys[:, 0], 1e-6)) * 100 + rel_err_power = np.mean( + np.abs(Y_nn[:, 1] - Y_phys[:, 1]) / np.maximum(Y_phys[:, 1], 1e-6)) * 100 + + if verbose: + print(f"-> Distillation complete!") + print(f" Fuel Flow MAPE: {rel_err_fuel:.2f}%") + print(f" Power MAPE: {rel_err_power:.2f}%") + + # 保存(始终保存 CPU 版,推理时无需 GPU 环境) + if nn_save_path: + os.makedirs(os.path.dirname(os.path.abspath(nn_save_path)), exist_ok=True) + torch.save(nn_model_cpu.state_dict(), nn_save_path) + if verbose: + print(f"-> Saved NN model to {nn_save_path}") + + return { + 'loss_history': loss_history, + 'nn_model': nn_model_cpu, + 'X_train': X_phys, + 'Y_train': Y_phys, + 'Y_nn': Y_nn, + 'scaler_X': scaler_X, + 'scaler_Y': scaler_Y, + 'rel_error_fuel': rel_err_fuel, + 'rel_error_power': rel_err_power, + } + + +if __name__ == "__main__": + model_root = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..") + csv_path = os.path.join(model_root, "data", "Cleaned_Engine_Data_Full.csv") + nn_path = os.path.join(model_root, "data", "engine_nn_proxy.pth") + + # 使用 CSV 直接训练模式 (无需 sklearn/botorch) + result = distill_from_csv(csv_path, nn_path, epochs=3000) + print(f"\nFinal training loss: {result['loss_history'][-1]:.6f}") diff --git a/Model/src/engine_dynamic_sim.py b/Model/src/engine_dynamic_sim.py index d2e52a7..3fd28db 100644 --- a/Model/src/engine_dynamic_sim.py +++ b/Model/src/engine_dynamic_sim.py @@ -1,100 +1,72 @@ import sys import os + sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) import numpy as np +import torch import matplotlib.pyplot as plt -from src.engine_gpr_class import EngineGPRModel -from src.increPID import IncrementalPIDController +from tqdm import tqdm +from src.lightweight_model import EngineNNProxy +from src.mpc_controller import TurboShaftMPCController class TurboshaftDynamicSim: """ 涡轴发动机动态仿真类 - 包含一阶燃油执行机构和转子动力学模型 + + 包含功能: + 1. 基于 NN Proxy (轻量化神经网络) 的稳态代理模型 + 2. 一阶燃油执行机构动态 (First-order Actuator Dynamics) + 3. 转子动力学积分 (Rotor Dynamics) + 4. MPC (模型预测控制) 用于功率跟随 """ - def __init__(self, gpr_csv_path="data/Cleaned_Engine_Data_Full.csv", gpr_pth_path="data/engine_gpr_model.pth", + def __init__(self, nn_pth_path="data/engine_nn_proxy.pth", tau_fuel=0.15, K_inertia=100.0, - kp=4.652, ki=7.078, kd=0.222, min_fuel=10.0, max_fuel=600.0, - verbose=False): - """ - 初始化仿真环境和代理模型 - :param gpr_csv_path: GPR模型使用的数据集路径 - :param gpr_pth_path: 预训练的模型参数路径 - :param tau_fuel: 燃油执行机构时间常数 (s) - :param K_inertia: 转子惯性增益 (RPM / (kg/h)) - :param kp: PID比例系数 (用于功率跟随控制,归一化域) - :param ki: PID积分系数 (归一化域) - :param kd: PID微分系数 (归一化域) - :param min_fuel: 燃油流量下限 (kg/h) - :param max_fuel: 燃油流量上限 (kg/h) - :param verbose: 是否打印详细信息 - """ - import os - self.verbose = verbose - - if verbose: - print("-> 正在加载 GPR 稳态代理模型...") - self.engine_model = EngineGPRModel(gpr_csv_path) - - # 检查模型文件是否存在,不存在则训练 - if not os.path.exists(gpr_pth_path): - if verbose: - print(f"-> 模型文件 {gpr_pth_path} 不存在,正在训练模型...") - self.engine_model.train(save_path=gpr_pth_path) - else: - success = self.engine_model.load_model(gpr_pth_path) - if not success: - if verbose: - print(f"-> 模型加载失败,正在重新训练...") - self.engine_model.train(save_path=gpr_pth_path) + mpc_horizon=15, mpc_dt=0.02, min_fuel=10.0, max_fuel=400.0, + mpc_overshoot_limit=0.05): + print("-> 正在加载 NN 稳态代理模型...") + self.engine_model = EngineNNProxy() + + if not os.path.exists(nn_pth_path): + raise FileNotFoundError(f"Missing NN model file: {nn_pth_path}. Please run scripts/distill_gpr_to_nn.py first.") + + self.engine_model.load_state_dict(torch.load(nn_pth_path, map_location='cpu')) + self.engine_model.eval() + print("-> NN Model Loaded (CPU Mode for Sim Loop)") - # 动态参数 self.tau_fuel = tau_fuel self.K_inertia = K_inertia - - # 归一化基准 (用于PID计算) self.max_fuel = max_fuel - self.max_power_ref = 300.0 # 300kw为功率归一化基准,根据实际数据调整 + self.max_power_ref = 300.0 - # 控制器初始化 (使用归一化参数,通过 scaling 自动处理) - self.pid = IncrementalPIDController( - kp=kp, ki=ki, kd=kd, dt=0.01, - output_min=min_fuel, output_max=max_fuel, - input_scale=self.max_power_ref, output_scale=self.max_fuel + self.mpc = TurboShaftMPCController( + tau_fuel=tau_fuel, K_inertia=K_inertia, dt=mpc_dt, horizon=mpc_horizon, + min_fuel=min_fuel, max_fuel=max_fuel, overshoot_limit=mpc_overshoot_limit ) - # 环境与状态变量 self.H_env = 0.0 self.Ma_env = 0.0 self.N_current = 0.0 self.Wf_act_current = 0.0 self.power_generated = 0.0 - - # 控制指令 self.Wf_cmd = 0.0 def _solve_steady_rpm(self, target_val, target_type='power'): - """ - 内部方法:反解稳态转速 (RPM) - :param target_val: 目标值 (Power[kW] 或 Fuel[kg/h]) - :param target_type: 'power' 或 'fuel' - :return: 对应的稳态转速 - """ + """数值反解给定功率/燃油下的稳态转速""" from scipy.optimize import brentq - - # 搜索范围 [RPM_min, RPM_max],根据经验或数据范围设定 low_bound, high_bound = 0.0, 60000.0 def objective(n): - current_input = np.array([[self.H_env, self.Ma_env, n]]) - pred_mean, _ = self.engine_model.predict(current_input) + # 构造输入: [H, Ma, N] + current_input = torch.tensor([[self.H_env, self.Ma_env, n]], dtype=torch.float32) + with torch.no_grad(): + pred_mean = self.engine_model(current_input).numpy() - # index 0: Fuel Flow (kg/h), index 1: Power (kW) + # 输出: [0]=Fuel, [1]=Power val = pred_mean[0, 1] if target_type == 'power' else pred_mean[0, 0] return val - target_val try: - # 简单的边界检查,防止报错 f_low = objective(low_bound) f_high = objective(high_bound) if f_low * f_high > 0: @@ -108,118 +80,104 @@ class TurboshaftDynamicSim: return (low_bound + high_bound) / 2.0 def set_steady_state_by_power(self, H_env, Ma_env, Power_target): - """ - 通过目标功率初始化稳态 - :param Power_target: 目标轴功率 (kW) - :return: 对应的稳态转速 (RPM) - """ + """设定初始稳态工况点""" self.H_env = H_env self.Ma_env = Ma_env - - # 反解转速 self.N_current = self._solve_steady_rpm(Power_target, target_type='power') - # 计算该转速下的稳态燃油 - current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) - pred_mean, _ = self.engine_model.predict(current_input) + # 计算该稳态下的燃油消耗 + current_input = torch.tensor([[self.H_env, self.Ma_env, self.N_current]], dtype=torch.float32) + with torch.no_grad(): + pred_mean = self.engine_model(current_input).numpy() self.power_generated = pred_mean[0, 1] self.Wf_act_current = pred_mean[0, 0] self.Wf_cmd = self.Wf_act_current - print(f"-> 稳态(Power)已配置: H={H_env}, Ma={Ma_env}, Target_P={Power_target:.1f} kW => N={self.N_current:.1f} RPM, Wf={self.Wf_cmd:.2f} kg/h") if self.verbose else None - - # 重置PID控制器至当前稳态输出 (自动处理归一化) - self.pid.reset(initial_output=self.Wf_cmd) - + print(f"-> 稳态(Power)已配置: H={H_env}, Ma={Ma_env}, Target_P={Power_target:.1f} kW => N={self.N_current:.1f} RPM, Wf={self.Wf_cmd:.2f} kg/h") + self.mpc.reset(initial_output=self.Wf_cmd, initial_N=self.N_current) return self.N_current - def set_steady_state_by_fuel(self, H_env, Ma_env, Wf_target): - """ - 通过目标燃油流量初始化稳态 - :param Wf_target: 目标燃油流量 (kg/h) - :return: 对应的稳态转速 (RPM) - """ - self.H_env = H_env - self.Ma_env = Ma_env - - # 反解转速 - self.N_current = self._solve_steady_rpm(Wf_target, target_type='fuel') - - # 确认该状态下的功率 - current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) - pred_mean, _ = self.engine_model.predict(current_input) - - self.power_generated = pred_mean[0, 1] - self.Wf_act_current = pred_mean[0, 0] # 理论上应该非常接近 Wf_target - self.Wf_cmd = self.Wf_act_current - - print(f"-> 稳态(Fuel)已配置: H={H_env}, Ma={Ma_env}, Target_Wf={Wf_target:.1f} kg/h => N={self.N_current:.1f} RPM, Power={self.power_generated:.1f} kW") if self.verbose else None - - # 重置PID控制器至当前稳态输出 (自动处理归一化) - self.pid.reset(initial_output=self.Wf_cmd) - - return self.N_current - - def set_fuel_command(self, Wf_cmd): - """ - 更改燃油流量指令 - :param Wf_cmd: 目标燃油流量指令 - """ - self.Wf_cmd = Wf_cmd - - def set_flight_condition(self, H_env=None, Ma_env=None): - """ - 在运行过程中更改当前的飞行条件 (高度和马赫数) - :param H_env: 新的飞行高度 (m)。如果为 None,则保持不变。 - :param Ma_env: 新的飞行马赫数。如果为 None,则保持不变。 - """ - if H_env is not None: - self.H_env = H_env - if Ma_env is not None: - self.Ma_env = Ma_env - - def compute_control_law(self, dt, target_power): - """ - 计算控制律 (PID控制: Power -> Wf) - :param dt: 控制周期 (s) - :param target_power: 期望功率 (kW) - :return: 计算出的燃油指令 - """ - self.pid.dt = dt - # 1. 计算控制增量 (PID内部会自动处理归一化) - self.Wf_cmd = self.pid.compute(setpoint=target_power, measurement=self.power_generated) + def compute_control_law(self, dt, target_power, precalc_params=None): + """调用 MPC 更新控制指令""" + self.Wf_cmd = self.mpc.compute( + current_N=self.N_current, + current_Wfact=self.Wf_act_current, + target_power=target_power, + precalc_params=precalc_params + ) return self.Wf_cmd def step(self, dt, target_power=None): """ - 执行单步动态仿真 - :param dt: 积分步长 (s) - :param target_power: 目标轴功率 (kW),若不为 None 则执行一次PID控制 - :return: (当前转速, 实际供油量, 当前需要的平衡供油量, 当前功率) + 执行单步动态仿真 (High-Performance Optimized) + + 加速策略: + - 聚合 GPR 预测请求: 将 MPC 所需的梯度计算点与当前物理状态点合并为一个 Batch (Size=2) + - 减少 GPU I/O 次数: 从每步 3 次减少为 1 次 """ - # 0. 闭环控制计算 - if target_power is not None: - self.compute_control_law(dt, target_power) + + # --- 0. 统一 GPU 批次预测 (Batch Prediction) --- + # 构造输入: [Row 0: 当前状态点, Row 1: 用于梯度计算的微扰点] + delta_N = 5.0 - # 1. 燃油执行机构动态 (一阶惯性) + # 判断模型类型: NN 模型使用 forward,GPR 模型使用 predict + if hasattr(self.engine_model, 'predict') and not hasattr(self.engine_model, 'forward'): + # GPR 模型 + inputs = np.array([ + [self.H_env, self.Ma_env, self.N_current], + [self.H_env, self.Ma_env, self.N_current + delta_N] + ]) + pred_mean, _ = self.engine_model.predict(inputs) + else: + # NN 模型 + import torch + inputs = torch.tensor([ + [self.H_env, self.Ma_env, self.N_current], + [self.H_env, self.Ma_env, self.N_current + delta_N] + ], dtype=torch.float32) + with torch.no_grad(): + pred_mean = self.engine_model(inputs).numpy() + + # 核心加速点:一次 GPU 调用获取所有信息 + # pred_mean 形如 [[Wf0, Pow0], [Wf1, Pow1]] + + # 提取结果 + Wf_req_current = pred_mean[0, 0] + Power_current = pred_mean[0, 1] + + Wf_req_pert = pred_mean[1, 0] + Power_pert = pred_mean[1, 1] + + # --- 1. 闭环控制计算 --- + if target_power is not None: + # 在 Python 端快速计算梯度,避免在 MPC 内部再次调用模型 + k_wf = (Wf_req_pert - Wf_req_current) / delta_N + k_p = (Power_pert - Power_current) / delta_N + + # 使用预计算好的参数,MPC 内部将不再调用 engine_model.predict + params = (Wf_req_current, Power_current, k_wf, k_p) + self.compute_control_law(dt, target_power, precalc_params=params) + + # --- 2. 燃油执行机构动态 (一阶惯性) --- dWf_act_dt = (self.Wf_cmd - self.Wf_act_current) / self.tau_fuel Wf_act_next = self.Wf_act_current + dWf_act_dt * dt - # 2. 调用GPR代理模型计算当前转速下的阻力矩(需求燃油) - current_input = np.array([[self.H_env, self.Ma_env, self.N_current]]) - pred_mean, _ = self.engine_model.predict(current_input) - - # GPR 输出: [0]: Fuel Flow, [1]: Shaft Power - Wf_req_current = pred_mean[0, 0] #维持当前转速所需的稳态燃油 + # --- 3. 调用NN代理模型推算当前气动热力参数 --- + current_input = torch.tensor([[self.H_env, self.Ma_env, self.N_current]], dtype=torch.float32) + with torch.no_grad(): + pred_mean = self.engine_model(current_input).numpy() + + # [0]: Fuel Flow (kg/h), [1]: Power (kW) + Wf_req_current = pred_mean[0, 0] Power_current = pred_mean[0, 1] - self.power_generated = Power_current # 更新当前功率状态 + self.power_generated = Power_current - # 3. 转子动力学积分 (燃料差额 -> 转速加速度) + # --- 4. 转子动力学积分 (供油盈余 -> 加速) --- dN_dt = self.K_inertia * (self.Wf_act_current - Wf_req_current) N_next = self.N_current + dN_dt * dt - # 4. 状态更新 + # 更新状态 self.Wf_act_current = Wf_act_next self.N_current = N_next @@ -228,7 +186,8 @@ class TurboshaftDynamicSim: if __name__ == "__main__": # ========================================== - # 【测试示例】利用类运行功率闭环控制仿真 + # 涡轴发动机动态响应测试脚本 + # 模拟复杂剖面: 包含阶跃、正弦、斜坡指令及变高度/马赫数干扰 # ========================================== import matplotlib matplotlib.use('Agg') @@ -238,121 +197,105 @@ if __name__ == "__main__": plt.rcParams['font.serif'] = ['DejaVu Serif', 'Times New Roman'] plt.rcParams['axes.unicode_minus'] = True - # 初始化仿真 (优化后的PID参数) - # kp: 比例系数 - 增大以加快响应速度 - # ki: 积分系数 - 适中以消除稳态误差 - # kd: 微分系数 - 设为0避免控制振荡 - sim = TurboshaftDynamicSim() + sim = TurboshaftDynamicSim(mpc_dt=0.02) - # 1. 设置初始稳态 (通过目标功率设定) - P_initial_target = 100.0 # kW + # 初始状态 + P_initial_target = 100.0 sim.set_steady_state_by_power(H_env=0.0, Ma_env=0.0, Power_target=P_initial_target) - # 仿真参数 dt = 0.02 - t_end = 30.0 # 增加仿真时间以展示更多指令变化 + t_end = 60.0 time_array = np.arange(0, t_end, dt) - # 数据记录 - N_log = [] - Wf_act_log = [] - Wf_cmd_log = [] - Power_log = [] - Power_target_log = [] + N_log, Wf_act_log, Wf_cmd_log, Power_log, Power_target_log = [], [], [], [], [] + H_env_log, Ma_env_log = [], [] - # 定义多段功率指令 (时间[s], 目标功率[kW]) - power_profile = [ - (0.0, 100.0), # 初始稳态 - (3.0, 200.0), # 阶跃上升 - (8.0, 150.0), # 阶跃下降 - (13.0, 250.0), # 阶跃上升至高功率 - (18.0, 100.0), # 快速下降 - (23.0, 180.0), # 再次上升 - ] + print("-> 开始极限工况仿真测试 (大动态指令 + 连续外界干扰)...") - # 斜坡指令测试:从18kW开始以一定速率上升 - ramp_start_time = 25.0 - ramp_rate = 10.0 # kW/s - - # 设定飞行条件改变的时间 - flight_change_time = 28.0 - - def get_target_power(t, profile, ramp_start, ramp_rate, default_power): - """根据时间获取当前目标功率""" - for i, (time, _) in enumerate(profile): - if t < time: - return profile[i-1][1] if i > 0 else default_power - # 如果在斜坡区间 - if t >= ramp_start: - last_static_power = profile[-1][1] - ramp_power = last_static_power + ramp_rate * (t - ramp_start) - return min(ramp_power, 300.0) # 限制最大300kW - return profile[-1][1] - - print("-> 开始仿真步进 (闭环控制)...") - print("-> 功率指令配置文件:") - for time, power in power_profile: - print(f" t={time:.1f}s -> {power:.0f} kW") - print(f" t={ramp_start_time:.1f}s -> 斜坡上升 (速率 {ramp_rate} kW/s)") - print() - - for t in time_array: - # 确定当前的目标功率 - current_target_P = get_target_power(t, power_profile, ramp_start_time, ramp_rate, P_initial_target) + for t in tqdm(time_array, desc="Simulating"): + # --- 1. 生成复杂功率指令 (Setpoint) --- + if t < 10.0: + # 阶跃测试 + target_p = 100.0 if t < 3 else 220.0 if t < 7 else 80.0 + elif t < 25.0: + # 正弦跟踪 (0.2Hz) + target_p = 140.0 + 50.0 * np.sin(2 * np.pi * 0.2 * (t - 10.0)) + elif t < 40.0: + # 锯齿波测试 + cycle = (t - 25.0) % 5.0 + target_p = 80.0 + (80.0 / 5.0) * cycle + else: + # 极限大范围跳变 + target_p = 170.0 if t < 48.0 else 40.0 - # 触发飞行条件变化 (确保仅触发一次以免重复打印) - if flight_change_time <= t < flight_change_time + dt: - print(f"\n[!] 时间 t={t:.2f}s, 模拟飞行条件跃变...") - sim.set_flight_condition(H_env=3000.0, Ma_env=0.2) + # --- 2. 生成环境扰动 (Disturbance) --- + if t < 15.0: + h_env, ma_env = 0.0, 0.0 + elif t < 30.0: + # 爬升阶段: 0->3000m + h_env = 0.0 + (3000.0 / 15.0) * (t - 15.0) + ma_env = 0.0 + (0.2 / 15.0) * (t - 15.0) + elif t < 45.0: + h_env, ma_env = 3000.0, 0.2 + else: + # 突发机动 + h_env, ma_env = 500.0, 0.4 + + sim.H_env = h_env + sim.Ma_env = ma_env - # 执行闭环仿真步进 - N_cur, Wf_act_cur, Wf_req, Power_cur = sim.step(dt, target_power=current_target_P) + # 执行单步仿真 + N_cur, Wf_act_cur, Wf_req, Power_cur = sim.step(dt, target_power=target_p) - # 记录数据 N_log.append(N_cur) Wf_act_log.append(Wf_act_cur) Wf_cmd_log.append(sim.Wf_cmd) Power_log.append(Power_cur) - Power_target_log.append(current_target_P) + Power_target_log.append(target_p) + H_env_log.append(h_env) + Ma_env_log.append(ma_env) - # 绘图 - fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(12, 10)) - fig.suptitle(f'Turboshaft Engine Dynamic Simulation\nPID Parameters: Kp={sim.pid.kp}, Ki={sim.pid.ki}, Kd={sim.pid.kd}', - fontsize=12, fontweight='bold') + # ========================================== + # 绘图逻辑 + # ========================================== + fig, axes = plt.subplots(4, 1, figsize=(14, 12), gridspec_kw={'height_ratios': [2.5, 2, 2, 1.5]}) + fig.suptitle('Turboshaft Engine: MPC Extreme Stress Test\nSine Tracking + Continuous Disturbances', + fontsize=14, fontweight='bold') - # Plot 1: Power Tracking - ax1.plot(time_array, Power_target_log, 'k--', linewidth=2, label='Target Power') - ax1.plot(time_array, Power_log, 'g-', linewidth=2, label='Actual Power') - ax1.set_ylabel('Shaft Power [kW]') - ax1.set_title('Closed-Loop Control: Power Tracking', fontweight='bold') - ax1.grid(True, linestyle=':', alpha=0.7) - ax1.legend() + # Plot 1: Power Tracking (核心表现) + axes[0].plot(time_array, Power_target_log, 'k--', linewidth=2, label='Target Power (Command)') + axes[0].plot(time_array, Power_log, 'g-', linewidth=2, label='Actual Power (MPC)') + axes[0].set_ylabel('Shaft Power [kW]', fontweight='bold') + axes[0].set_title('Performance: Complex Trajectory Tracking', fontweight='bold') + axes[0].grid(True, linestyle=':', alpha=0.7) + axes[0].legend(loc='upper right') # Plot 2: Rotor Speed - ax2.plot(time_array, N_log, 'b-', linewidth=2, label='Engine Speed (N)') - ax2.set_ylabel('Rotor Speed [RPM]') - ax2.set_title('Engine Response: Rotor Speed', fontweight='bold') - ax2.grid(True, linestyle=':', alpha=0.7) - ax2.legend() + axes[1].plot(time_array, N_log, 'b-', linewidth=2, label='Engine Speed (N)') + axes[1].set_ylabel('Rotor Speed [RPM]', fontweight='bold') + axes[1].set_title('State: Rotor Speed Response', fontweight='bold') + axes[1].grid(True, linestyle=':', alpha=0.7) + axes[1].legend(loc='upper right') # Plot 3: Fuel Flow (Control Input) - ax3.plot(time_array, Wf_cmd_log, 'k--', linewidth=1.5, label='Fuel Command') - ax3.plot(time_array, Wf_act_log, 'r-', linewidth=2, label='Actual Fuel') - ax3.set_xlabel('Time [s]') - ax3.set_ylabel('Fuel Flow [kg/h]') - ax3.set_title('Control Effort: Fuel Flow', fontweight='bold') - ax3.grid(True, linestyle=':', alpha=0.7) - ax3.legend() + axes[2].plot(time_array, Wf_cmd_log, 'r--', linewidth=1.5, label='Fuel Command (MPC Output)') + axes[2].plot(time_array, Wf_act_log, 'm-', linewidth=2, label='Actual Fuel Actuator') + axes[2].set_ylabel('Fuel Flow [kg/h]', fontweight='bold') + axes[2].set_title('Control Effort: Actuator Dynamics', fontweight='bold') + axes[2].grid(True, linestyle=':', alpha=0.7) + axes[2].legend(loc='upper right') + + # Plot 4: Environmental Disturbances + ax4_1 = axes[3] + ax4_2 = ax4_1.twinx() + ax4_1.plot(time_array, H_env_log, 'c-', linewidth=2, label='Altitude (m)') + ax4_2.plot(time_array, Ma_env_log, 'y-', linewidth=2, label='Mach Number') + ax4_1.set_xlabel('Time [s]', fontweight='bold') + ax4_1.set_ylabel('Altitude [m]', color='c', fontweight='bold') + ax4_2.set_ylabel('Mach', color='y', fontweight='bold') + axes[3].set_title('Disturbances: Flight Conditions', fontweight='bold') + axes[3].grid(True, linestyle=':', alpha=0.7) plt.tight_layout(rect=[0, 0, 1, 0.96]) - plt.savefig('figures/engine_dynamic_sim_plot.png') - print('Plot saved to figures/engine_dynamic_sim_plot.png') - - # 保存数据到 .dat 文件 - dat_file = 'data/pid_tuning_data.dat' - with open(dat_file, 'w') as f: - f.write('# Time[s]\tTarget_Power[kW]\tActual_Power[kW]\tRotor_Speed[RPM]\tFuel_Command[kg/h]\tActual_Fuel[kg/h]\n') - for i in range(len(time_array)): - f.write(f'{time_array[i]:.4f}\t{Power_target_log[i]:.2f}\t{Power_log[i]:.2f}\t{N_log[i]:.2f}\t{Wf_cmd_log[i]:.2f}\t{Wf_act_log[i]:.2f}\n') - print(f'Data saved to {dat_file}') - # plt.show() + plt.savefig('figures/engine_mpc_stress_test.png', dpi=200) + print('-> 仿真完成!极限制图已保存至: figures/engine_mpc_stress_test.png') diff --git a/Model/src/engine_gpr_class.py b/Model/src/engine_gpr_class.py index ab18054..b8e52dc 100644 --- a/Model/src/engine_gpr_class.py +++ b/Model/src/engine_gpr_class.py @@ -34,10 +34,13 @@ class EngineGPRModel: self.scaler_X = StandardScaler() self.scaler_Y = StandardScaler() self.model = None + # 自动检测并使用 GPU + self.device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Using device: {self.device}") def _prepare_data(self): """数据预处理:读取、筛选、Log变换、标准化""" - self.df = pd.read_csv(self.csv_path) + self.df = pd.read_csv(self.csv_path, encoding='utf-8') X_df = self.df[['Altitude_m', 'Mach', 'RPM']] # 剔除无效特征 @@ -56,7 +59,8 @@ class EngineGPRModel: X_scaled = self.scaler_X.fit_transform(X_numpy) Y_scaled = self.scaler_Y.fit_transform(Y_numpy) - return torch.tensor(X_scaled, dtype=torch.double), torch.tensor(Y_scaled, dtype=torch.double) + # 转换为 Tensor 并移动到 GPU (如果可用) + return torch.tensor(X_scaled, dtype=torch.double).to(self.device), torch.tensor(Y_scaled, dtype=torch.double).to(self.device) def _init_model(self, train_X, train_Y): """内部方法:统一初始化模型结构(包含 Mean 和 Prior 设置)""" @@ -83,6 +87,7 @@ class EngineGPRModel: # 使用统一初始化方法 self.model = self._init_model(train_X, train_Y) + self.model.to(self.device) # 仅在训练开始前设定初始值,引导优化方向 if hasattr(self.model.covar_module, 'base_kernel'): @@ -109,10 +114,12 @@ class EngineGPRModel: train_X, train_Y = self._prepare_data() # 必须使用完全相同的结构初始化,否则 load_state_dict 会报错 self.model = self._init_model(train_X, train_Y) + self.model.to(self.device) # 使用 strict=False 忽略 Prior 缓冲区的差异(例如 _transformed_loc 等内部参数) # 这些参数通常不影响模型预测,只影响后续继续训练时的约束 - self.model.load_state_dict(torch.load(pth_path), strict=True) + state_dict = torch.load(pth_path, map_location=self.device) + self.model.load_state_dict(state_dict, strict=True) self.model.eval() print("-> Model loaded successfully.") return True @@ -125,12 +132,19 @@ class EngineGPRModel: if self.model is None: raise ValueError("Model not initialized.") self.model.eval() - test_X_scaled = torch.tensor(self.scaler_X.transform(test_X_real), dtype=torch.double) - + + # Determine device from model parameters + try: + device = next(self.model.parameters()).device + except StopIteration: + device = torch.device('cpu') + + test_X_scaled = torch.tensor(self.scaler_X.transform(test_X_real), dtype=torch.double, device=device) + with torch.no_grad(): posterior = self.model.posterior(test_X_scaled) - mu_scaled = posterior.mean.numpy() - var_scaled = posterior.variance.numpy() + mu_scaled = posterior.mean.detach().cpu().numpy() + var_scaled = posterior.variance.detach().cpu().numpy() # 反归一化 mu_log_real = self.scaler_Y.inverse_transform(mu_scaled) diff --git a/Model/src/lightweight_model.py b/Model/src/lightweight_model.py new file mode 100644 index 0000000..84cd510 --- /dev/null +++ b/Model/src/lightweight_model.py @@ -0,0 +1,70 @@ +import torch +import torch.nn as nn +import numpy as np + +class EngineNNProxy(nn.Module): + """ + 轻量级神经网络代理模型,用于替代笨重的 GPR 模型。 + 结构: 简单的 MLP (多层感知机) + 输入: [Altitude, Mach, RPM] (未归一化) + 输出: [FuelFlow, Power] (未归一化) + """ + def __init__(self, hidden_size=64): + super(EngineNNProxy, self).__init__() + + # 定义网络结构 + # 对应 distill_gpr_to_nn.py 中的索引访问: + # 0: Linear + # 1: ReLU / Tanh + # 2: Linear + # 3: ReLU / Tanh + # 4: Linear (Output) + self.net = nn.Sequential( + nn.Linear(3, hidden_size), # 0 + nn.Tanh(), # 1: Tanh 通常比 ReLU 更适合平滑的物理函数拟合 + nn.Linear(hidden_size, hidden_size), # 2 + nn.Tanh(), # 3 + nn.Linear(hidden_size, 2) # 4: 输出 2 个物理量 (Fuel, Power) + ) + + # 归一化参数 (注册为 buffer 以便随模型保存) + # 初始化为默认值,防止未调用 set_normalization_params 时报错 + self.register_buffer('x_mean', torch.zeros(3)) + self.register_buffer('x_scale', torch.ones(3)) + self.register_buffer('y_mean', torch.zeros(2)) + self.register_buffer('y_scale', torch.ones(2)) + + def set_normalization_params(self, scaler_X, scaler_Y): + """ + 从 sklearn StandardScaler 中提取参数 + scaler_X: 用于输入的归一化器 + scaler_Y: 用于输出 (Log1p Space) 的归一化器 + """ + if scaler_X is not None: + self.x_mean.copy_(torch.tensor(scaler_X.mean_, dtype=torch.float32)) + self.x_scale.copy_(torch.tensor(scaler_X.scale_, dtype=torch.float32)) + + if scaler_Y is not None: + self.y_mean.copy_(torch.tensor(scaler_Y.mean_, dtype=torch.float32)) + self.y_scale.copy_(torch.tensor(scaler_Y.scale_, dtype=torch.float32)) + + def forward(self, x): + """ + 前向传播: 物理输入 -> 物理输出 + 包含: 归一化 -> NN推理 -> 反归一化 -> expm1 + """ + # 1. 输入归一化 (Z-Score) + # 确保输入 x 与 buffer 在同一设备 + x = x.to(self.x_mean.device) + x_norm = (x - self.x_mean) / self.x_scale + + # 2. 神经网络推理 (预测 Log Normalized Z-Score) + y_norm_pred = self.net(x_norm) + + # 3. 输出反归一化 (Z-Score Inverse) + y_log1p_pred = y_norm_pred * self.y_scale + self.y_mean + + # 4. 指数还原 (Inverse Log1p) + y_phys_pred = torch.expm1(y_log1p_pred) + + return y_phys_pred diff --git a/Model/src/motor_sim.py b/Model/src/motor_sim.py index 122716e..9fdfaa1 100644 --- a/Model/src/motor_sim.py +++ b/Model/src/motor_sim.py @@ -9,18 +9,16 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 模块提供了永磁同步电机 (PMSM) 的离散时间仿真实现,涵盖了闭环转速控制、 物理级的电磁转矩估算及端电压/损耗模型的动力学计算。 -系统符号学约定: +系统符号约定: - 轴系转矩 (T_motor): 正值表示吸收轴系功率 (发电机/负载响应),负值表示向轴系输出功率 (驱动动力)。 - 直流母线功率 (P_bus): 正值表示从母线汲取有功功率,负值表示向母线回馈有功功率。 """ -from typing import Any, Dict -import sys -import os -sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +from typing import Any, Dict, Optional import numpy as np -from src.increPID import IncrementalPIDController +from tqdm import tqdm +from src.mpc_controller import MotorMPCController class MotorSim: @@ -40,12 +38,7 @@ class MotorSim: eta_mot: float = 0.95, eta_gen: float = 0.93, B_visc: float = 3e-4, - k_p_w: float = 13.440362, - k_i_w: float = 42.997816, - k_d_w: float = 0.484660, - k_d_w_error: float = 0.05, p_bus_slew_rate_kw: float = 2000.0, - t_cmd_slew_rate_nm_s: float = 0.0, tau_i: float = 0.004, k_mod: float = 1 / np.sqrt(3), P_const_loss: float = 250.0, @@ -53,9 +46,11 @@ class MotorSim: k_inv: float = 0.015, tau_n_ref: float = 0.35, tau_t_cmd: float = 0.12, - speed_priority_band_rpm: float = 180.0, tau_v_bus: float = 0.08, - i_s_max: float | None = None, + i_s_max: Optional[float] = None, + mpc_W_speed: float = 0.0, + mpc_W_dcost: float = 0.0, + mpc_overshoot_limit: float = 0.05, ): """ 初始化电机动力学仿真器参数。 @@ -72,12 +67,7 @@ class MotorSim: :param eta_mot: 稳态驱动效率参考值 :param eta_gen: 稳态发电效率参考值 :param B_visc: 轴系黏性摩擦阻尼系数 (N·m·s) - :param k_p_w: 速度控制环比例增益 - :param k_i_w: 速度控制环积分增益 - :param k_d_w: 速度控制环微分控制增益(闭环阻尼) - :param k_d_w_error: 目标速度微分前馈增益 :param p_bus_slew_rate_kw: 允许的最大母线请求功率变化率 (kW/s) - :param t_cmd_slew_rate_nm_s: 允许的最大转矩指令变化率 (Nm/s) :param tau_i: 闭环电流/电磁转矩等效一阶延迟时间常数 (s) :param k_mod: DC-AC 变换器电压利用系数 :param P_const_loss: 独立于工况的系统常量损耗 (W) @@ -85,9 +75,11 @@ class MotorSim: :param k_inv: DC-AC 逆变环节损耗系数 :param tau_n_ref: 目标转速给定指令一阶低通滤波时间常数 (s) :param tau_t_cmd: 转矩输出指令一阶低通滤波时间常数 (s) - :param speed_priority_band_rpm: 目标跟随允许容差基准带 (RPM) :param tau_v_bus: DC侧动态响应一阶滤波时间常数 (s) :param i_s_max: 定子相电流约束阈值有效值 (A) + :param mpc_W_speed: MPC转速控制权重 + :param mpc_W_dcost: MPC控制增量权重 + :param mpc_overshoot_limit: MPC超调量硬约束 (0.05 = 5%) """ self._validate_parameters( n_p, @@ -116,12 +108,7 @@ class MotorSim: self.eta_mot = eta_mot self.eta_gen = eta_gen self.B_visc = B_visc - self.k_p_w = k_p_w - self.k_i_w = k_i_w - self.k_d_w = k_d_w - self.k_d_w_error = k_d_w_error self.p_bus_slew_rate_kw = p_bus_slew_rate_kw - self.t_cmd_slew_rate_nm_s = max(0.0, float(t_cmd_slew_rate_nm_s)) self.tau_i = tau_i self.k_mod = k_mod self.P_const_loss = P_const_loss @@ -129,9 +116,11 @@ class MotorSim: self.k_inv = k_inv self.tau_n_ref = max(1e-4, float(tau_n_ref)) self.tau_t_cmd = max(1e-4, float(tau_t_cmd)) - self.speed_priority_band = max(5.0, float(speed_priority_band_rpm)) * 2.0 * np.pi / 60.0 self.tau_v_bus = max(1e-4, float(tau_v_bus)) + # 超调量约束参数 (用于转矩变化率限制) + self._overshoot_limit = mpc_overshoot_limit + self.tau_rate = self.P_rate / self.w_rate if i_s_max is None: base_current = self.tau_rate / (1.5 * self.n_p * max(abs(self.psi_f), 1e-6)) @@ -160,28 +149,20 @@ class MotorSim: self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0 self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5 self._sim_t = 0.0 - self._w_error_int = 0.0 - self._w_error_prev = 0.0 - self._dw_error_f = 0.0 - self._p_bus_req_prev = 0.0 self._w_set_f = 0.0 self._t_ref_f = 0.0 self._v_bus_f = float(self.u_dc) self._t_load_f = 0.0 self._t_ext_f = 0.0 - # 系统机械约束约束配置 + # 系统机械约束配置 self._dw_max = 800.0 - # 增量式PID控制器 (用于转速环功率控制) - # output_scale: 额定功率300kW,input_scale: 额定转速575.95 rad/s - self.speed_pid = IncrementalPIDController( - kp=k_p_w, ki=k_i_w, kd=k_d_w, - dt=0.02, # 将在step中动态更新 - output_min=-self.P_rate/1000.0 * 1.2, # 允许放电 - output_max=self.P_rate/1000.0 * 1.2, # 允许充电 - input_scale=self.w_rate, - output_scale=self.P_rate/1000.0 + # MPC控制器 (用于转速环功率控制) + self.mpc = MotorMPCController( + J=J, B_visc=B_visc, dt=0.02, horizon=10, + W_speed=mpc_W_speed, W_dcost=mpc_W_dcost, + overshoot_limit=mpc_overshoot_limit ) self._log = { @@ -208,7 +189,7 @@ class MotorSim: } # 滤波系数缓存 (用于优化: 避免每步重复计算 exp(-dt/tau)) - self._last_dt: float | None = None + self._last_dt: Optional[float] = None self._a_v: float = 0.0 # 母线电压滤波系数 self._a_n: float = 0.0 # 转速参考滤波系数 self._a_t_load: float = 0.0 # 负载转矩滤波系数 @@ -220,8 +201,6 @@ class MotorSim: self._EPSILON: float = 1e-6 # 数值 epsilon,用于避免除零 self._TAU_LOAD_FILTER: float = 0.05 # 负载转矩滤波时间常数 (s) self._TORQUE_OVERRATE: float = 1.35 # 转矩过载系数 - self._W_ERROR_DEADBAND: float = 0.05 # 转速误差死区 (rad/s) - self._DW_ERROR_FILTER: float = 0.15 # 微分误差滤波系数 # 日志记录配置:可选的最大日志长度限制 (None 表示无限制) self._log_maxlen: int | None = None # 可设置为如 100000 来限制内存使用 @@ -380,10 +359,11 @@ class MotorSim: w_abs = abs(w_M) w_e = self.n_p * w_abs - if w_e < self._EPSILON: + if w_e < 1e-6: return float(max(0.0, abs(self._torque_from_currents(0.0, self.i_s_max)))) - # 优化: 减少网格点数从40到12,使用更智能的搜索策略 + # 优化: 减少网格点数从40到12,在保证精度的同时提高效率 + # 使用更智能的搜索策略:从估计的MTPA点开始搜索 i_d_estimate = max(-self.i_s_max, min(0.0, -0.5 * (self.psi_f / (self.L_q - self.L_d)) if self.L_q != self.L_d else 0.0)) i_d_grid = np.array([ i_d_estimate - 0.3 * self.i_s_max, @@ -407,20 +387,14 @@ class MotorSim: tau_candidate = abs(self._torque_from_currents(i_d, i_q)) if tau_candidate > tau_best: tau_best = tau_candidate - if i_d < -0.01 * self.i_s_max: + if i_d < -0.01 * self.i_s_max: # 继续搜索更负的i_d continue tau_p_max = self.P_rate / max(w_abs, self._W_MIN_RAD) return float(max(0.0, min(tau_best, tau_p_max, self._TORQUE_OVERRATE * self.tau_rate))) - def _apply_power_slew(self, p_bus_req_kw: float, dt: float) -> float: - """对母线请求功率施加斜率限制。""" - if self.p_bus_slew_rate_kw <= 0: - self._p_bus_req_prev = float(p_bus_req_kw) - return float(p_bus_req_kw) - - delta = self.p_bus_slew_rate_kw * dt - self._p_bus_req_prev = float(p_bus_req_kw) + def _apply_power_slew(self, p_bus_req_kw: float) -> float: + """对母线请求功率施加斜率限制(当前实现为 passthrough)。""" return float(p_bus_req_kw) def _power_to_torque_ref(self, p_bus_w: float, w_M: float) -> float: @@ -434,13 +408,13 @@ class MotorSim: if abs(p_bus_w) < 1e-9: return 0.0 - w_eff = max(abs(w_M), 50.0) + w_eff = max(abs(w_M), self._W_MIN_RAD) if p_bus_w >= 0: # 取电模式:从母线取有功,经过内部热量/电磁损耗后,输出转动推力(负转矩) return -(p_bus_w * self.eta_mot) / w_eff else: # 发电模式:电机受到负负载强推,需要输出极大的正机械阻力(查表吸收电能) - return -p_bus_w / (w_eff * max(self.eta_gen, 1e-6)) + return -p_bus_w / (w_eff * max(self.eta_gen, self._EPSILON)) def _current_ref_from_torque(self, t_motor_ref: float, w_M: float, v_bus: float) -> tuple[float, float]: """ @@ -468,12 +442,14 @@ class MotorSim: iq_sign = 1.0 if i_q_ref >= 0 else -1.0 iq_abs_target = abs(i_q_ref) - # 优化: 减少网格点数从80到20 - for i_d in np.linspace(-self.i_s_max, 0.0, 20): + # 优化: 减少网格点数从80到20,使用更高效的搜索 + i_d_candidates = np.linspace(-self.i_s_max, 0.0, 20) + for i_d in i_d_candidates: iq_max = self._max_iq_given_id(i_d, w_e, v_bus) if iq_max >= iq_abs_target: return float(i_d), float(iq_sign * iq_abs_target) + # 如果没找到,使用最大弱磁点 i_d_fw = -self.i_s_max i_q_fw = iq_sign * self._max_iq_given_id(i_d_fw, w_e, v_bus) return float(i_d_fw), float(i_q_fw) @@ -560,10 +536,10 @@ class MotorSim: """ if dt <= 0: raise ValueError('dt must be positive') - if v_bus <= self._EPSILON: + if v_bus <= 1e-6: raise ValueError('v_bus must be positive') - # 更新滤波系数缓存 + # 更新滤波系数缓存 (仅当 dt 变化时重新计算) self._update_filter_coefficients(dt) self._v_bus_f += (float(v_bus) - self._v_bus_f) * self._a_v @@ -576,51 +552,29 @@ class MotorSim: self._w_set_f += (w_set_cmd - self._w_set_f) * self._a_n w_set = self._w_set_f - w_error = w_set - self.w_M - if abs(w_error) < self._W_ERROR_DEADBAND: - w_error = 0.0 - - dw_error = (w_error - self._w_error_prev) / max(dt, self._EPSILON) - self._w_error_prev = w_error - self._dw_error_f = self._dw_error_f * (1 - self._DW_ERROR_FILTER) + dw_error * self._DW_ERROR_FILTER - - self._w_error_int += w_error * dt p_lim_kw = (t_lim * max(self.w_M, self._W_MIN_RAD)) / 1000.0 / max(self.eta_mot, self._EPSILON) - int_limit = 1.2 * p_lim_kw / max(self.k_i_w, self._EPSILON) if self.k_i_w > 0 else 0.0 - self._w_error_int = np.clip(self._w_error_int, -int_limit, int_limit) self._t_load_f += (float(t_load) - self._t_load_f) * self._a_t_load self._t_ext_f += (float(t_ext) - self._t_ext_f) * self._a_t_load - # 阻力前馈折算为补偿所需电功率(kW) - t_ff = self._t_load_f + self._t_ext_f + self.B_visc * self.w_M - p_mech_ff_kw = (t_ff * max(self.w_M, 1e-3)) / 1000.0 - if p_mech_ff_kw > 0: - p_elec_ff_kw = p_mech_ff_kw / max(self.eta_mot, self._EPSILON) - else: - p_elec_ff_kw = p_mech_ff_kw * self.eta_gen - - # 阻力前馈折算为补偿所需电功率(kW) - t_ff = self._t_load_f + self._t_ext_f + self.B_visc * self.w_M - p_mech_ff_kw = (t_ff * max(self.w_M, 1e-3)) / 1000.0 - if p_mech_ff_kw > 0: - p_elec_ff_kw = p_mech_ff_kw / max(self.eta_mot, 1e-6) - else: - p_elec_ff_kw = p_mech_ff_kw * self.eta_gen - # ------------------------------------------------------------- - # A) 需求生成层: 以转速环计算下一拍应向电池索取的 P_bus_req_kw + # A) 需求生成层: 以转速环 MPC 计算下一拍应向电池索取的 P_bus_req_kw # ------------------------------------------------------------- - # 使用增量式PID计算控制量 - self.speed_pid.dt = dt - p_pid = self.speed_pid.compute(setpoint=w_set, measurement=self.w_M) + # 使用 MPC 控制器计算转矩指令,然后转换为功率请求 + t_motor_cmd = self.mpc.compute( + current_w=self.w_M, + target_w=w_set, + t_load=self._t_load_f, + t_ext=self._t_ext_f, + t_lim_upper=t_lim, + t_lim_lower=-t_lim + ) - # 添加微分前馈和阻力前馈 - p_d_ff = self.k_d_w_error * self._dw_error_f - - p_cmd_raw = p_pid + p_d_ff + p_elec_ff_kw + # 将 MPC 转矩指令转换为功率请求 (kW) + # 正转矩(发电) -> 吸收功率,负转矩(驱动) -> 输出功率 + p_cmd_raw = -t_motor_cmd * max(self.w_M, self._W_MIN_RAD) / 1000.0 p_bus_req_kw = float(np.clip(p_cmd_raw, -p_lim_kw, p_lim_kw)) - p_bus_req_kw = self._apply_power_slew(p_bus_req_kw, dt) + p_bus_req_kw = self._apply_power_slew(p_bus_req_kw) self.p_bus_req_kw = p_bus_req_kw @@ -630,6 +584,47 @@ class MotorSim: t_motor_ref = self._power_to_torque_ref(p_bus_actual_kw * 1000.0, self.w_M) t_motor_ref = float(np.clip(t_motor_ref, -t_lim, t_lim)) + # ------------------------------------------------------------- + # 超调量硬约束: 基于转矩变化率限制 + # 当接近目标转速时,限制转矩变化以防止超调 + # ------------------------------------------------------------- + w_target = w_set + w_max_allowed = w_target * (1 + self._overshoot_limit) # 105% + w_min_allowed = w_target * (1 - self._overshoot_limit) # 95% + + # 记录上一时刻的转矩参考 + t_motor_ref_prev = getattr(self, '_t_motor_ref_prev', 0.0) + + if w_target > self._W_MIN_RAD: + # 升速时: 如果当前转速超过允许最大值,强制减速 + if w_set > self.w_M and self.w_M > w_max_allowed: + t_motor_ref = -t_lim # 最大制动 + + # 降速时: 如果当前转速低于允许最小值,强制加速 + elif w_set < self.w_M and self.w_M < w_min_allowed: + t_motor_ref = t_lim # 最大驱动 + + # 接近目标时 (95%-105% 区间): 限制转矩变化率 + elif self.w_M > w_target * 0.90: + # 计算允许的最大转矩变化 + # 使转速不超过 w_max_allowed 的最大加速度 + max_allowed_dw = (w_max_allowed - self.w_M) / dt + max_allowed_T = -self.J * max_allowed_dw - t_load - t_ext - self.B_visc * self.w_M + + # 限制正转矩 (驱动) 不超过计算值 + if t_motor_ref < 0: # 驱动扭矩 + t_motor_ref = max(t_motor_ref, max_allowed_T) + + # 使转速不低于 w_min_allowed 的最小加速度 + min_allowed_dw = (w_min_allowed - self.w_M) / dt + min_allowed_T = -self.J * min_allowed_dw - t_load - t_ext - self.B_visc * self.w_M + + # 限制负转矩 (制动) 不超过计算值 + if t_motor_ref > 0: # 制动扭矩 + t_motor_ref = min(t_motor_ref, min_allowed_T) + + self._t_motor_ref_prev = t_motor_ref + self._t_ref_f += (t_motor_ref - self._t_ref_f) * self._a_t t_motor_ref_filtered = float(self._t_ref_f) @@ -760,12 +755,8 @@ class MotorSim: self.u_an, self.u_bn, self.u_cn = 0.0, 0.0, 0.0 self.duty_a, self.duty_b, self.duty_c = 0.5, 0.5, 0.5 self._sim_t = 0.0 - # 增量式PID控制器重置 - self.speed_pid.reset(initial_output=0.0) - self._w_error_int = 0.0 - self._w_error_prev = 0.0 - self._dw_error_f = 0.0 - self._p_bus_req_prev = 0.0 + # MPC控制器重置 + self.mpc.reset(initial_w=self.w_M) self._w_set_f = 0.0 self._t_ref_f = 0.0 self._v_bus_f = float(self.u_dc) @@ -774,231 +765,6 @@ class MotorSim: self._t_ext_f = 0.0 -def run_motor_test() -> Dict[str, Any]: - """基于接口的独立电机测试 - 复杂测试用例""" - import matplotlib.pyplot as plt - - print('=' * 60) - print('Motor Dynamic Test - Complex Profile') - print('=' * 60) - - # 使用优化后的PID参数 - motor = MotorSim( - P_rate=300e3, - w_rate=575.95, - J=0.8, - k_p_w=13.440362, - k_i_w=42.997816, - k_d_w=0.484660, - P_const_loss=300.0, - k_fe=4e-4, - ) - - dt = 0.02 - t_end = 100.0 - time_array = np.arange(0.0, t_end, dt) - - # 存储关键诊断数据 - torque_balance = [] - power_balance = [] - - p_actual_kw = 0.0 - for t in time_array: - # 复杂测试工况 - 多段转速变化 + 多种扰动 - # 目标转速曲线 - if t < 3.0: - n_setpoint = 0.0 - elif t < 8.0: - n_setpoint = 2000.0 - elif t < 15.0: - n_setpoint = 2800.0 - elif t < 22.0: - n_setpoint = 2200.0 - elif t < 32.0: - n_setpoint = 3500.0 - elif t < 42.0: - n_setpoint = 3800.0 - elif t < 52.0: - n_setpoint = 2800.0 - elif t < 62.0: - n_setpoint = 3000.0 - elif t < 72.0: - n_setpoint = 4000.0 - elif t < 82.0: - n_setpoint = 4200.0 - elif t < 92.0: - n_setpoint = 3200.0 - else: - n_setpoint = 3000.0 - - # 外部转矩扰动 - if t < 5.0: - t_ext = 25.0 - elif t < 12.0: - t_ext = 10.0 - elif t < 18.0: - t_ext = -30.0 - elif t < 25.0: - t_ext = 15.0 - elif t < 35.0: - t_ext = -50.0 - elif t < 45.0: - t_ext = 8.0 - elif t < 55.0: - t_ext = -20.0 - elif t < 65.0: - t_ext = 25.0 - elif t < 75.0: - t_ext = -60.0 - elif t < 85.0: - t_ext = 20.0 - else: - t_ext = 0.0 - - # 负载转矩 - 变化因子 - if t < 10.0: - t_load_factor = 1.0 - elif t < 22.0: - t_load_factor = 1.5 - elif t < 32.0: - t_load_factor = 0.8 - elif t < 42.0: - t_load_factor = 1.8 - elif t < 52.0: - t_load_factor = 1.0 - elif t < 62.0: - t_load_factor = 1.4 - elif t < 72.0: - t_load_factor = 0.9 - elif t < 82.0: - t_load_factor = 1.6 - else: - t_load_factor = 1.1 - - base_t_load = 16.0 + 0.020 * motor.w_M + 1.2e-5 * motor.w_M**2 - t_load = base_t_load * t_load_factor - - # 母线电压波动 - v_bus = 520.0 + 15.0 * np.sin(2 * np.pi * t / 8.0) + 5.0 * np.sin(2 * np.pi * t / 3.0) - - state = motor.step(dt, n_setpoint, p_actual_kw, v_bus, t_load, t_ext) - p_actual_kw = state['p_bus_req_kw'] - - torque_balance.append(state['torque_balance_error']) - power_balance.append(state['power_balance_error']) - - state = motor.get_state() - p_bus_kw = np.array(motor._log['p_bus_kw']) - p_req_kw = np.array(motor._log['p_bus_req_kw']) - p_shaft_kw = np.array(motor._log['p_shaft_kw']) - p_loss_kw = np.array(motor._log['p_loss_kw']) - n_log = np.array(motor._log['n_rpm']) - t_motor_log = np.array(motor._log['t_motor']) - t_load_log = np.array(motor._log['t_load']) - t_ext_log = np.array(motor._log['t_ext']) - v_bus_log = np.array(motor._log['v_bus']) - v_motor_log = np.array(motor._log['v_motor']) - i_bus_log = np.array(motor._log['i_bus_a']) - - # 计算性能指标 - n_setpoints = [] - for t in time_array: - if t < 3.0: - n_setpoints.append(0.0) - elif t < 8.0: - n_setpoints.append(2000.0) - elif t < 15.0: - n_setpoints.append(2800.0) - elif t < 22.0: - n_setpoints.append(2200.0) - elif t < 32.0: - n_setpoints.append(3500.0) - elif t < 42.0: - n_setpoints.append(3800.0) - elif t < 52.0: - n_setpoints.append(2800.0) - elif t < 62.0: - n_setpoints.append(3000.0) - elif t < 72.0: - n_setpoints.append(4000.0) - elif t < 82.0: - n_setpoints.append(4200.0) - elif t < 92.0: - n_setpoints.append(3200.0) - else: - n_setpoints.append(3000.0) - n_setpoints = np.array(n_setpoints) - - error = n_setpoints - n_log - ise = np.sum(error**2) * dt - iae = np.sum(np.abs(error)) * dt - - print(f"Final: speed={state['n_rpm']:.0f} RPM ({state['w_rad_s']:.1f} rad/s)") - print(f" t_motor={state['t_motor']:.2f} Nm, p_bus={state['p_bus_kw']:.2f} kW, v_motor={state['v_motor']:.1f} V") - print(f"Performance - ISE: {ise:.2f}, IAE: {iae:.2f}") - - # 诊断结果 - torque_balance = np.array(torque_balance) - power_balance = np.array(power_balance) - print(f"Diagnostic - Torque balance error (mean): {np.mean(np.abs(torque_balance)):.4f} Nm") - print(f"Diagnostic - Power balance error (mean): {np.mean(np.abs(power_balance)):.4f} W") - - fig, axes = plt.subplots(5, 1, figsize=(14, 14), sharex=True) - - axes[0].plot(time_array, n_setpoints, 'r--', lw=1.0, alpha=0.7, label='Setpoint') - axes[0].plot(time_array, n_log, 'b-', lw=1.6, label='Actual') - axes[0].set_ylabel('Speed [RPM]') - axes[0].set_title('Motor Speed - Complex Profile Test (100s)') - axes[0].grid(True, linestyle=':') - axes[0].legend() - - axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque') - axes[1].plot(time_array, t_load_log, 'k--', lw=1.0, label='Load Torque') - axes[1].plot(time_array, t_ext_log, color='tab:purple', lw=1.0, label='External Torque') - axes[1].set_ylabel('Torque [Nm]') - axes[1].set_title('Torque Balance') - axes[1].axhline(0.0, color='k', lw=0.6) - axes[1].grid(True, linestyle=':') - axes[1].legend() - - axes[2].plot(time_array, p_req_kw, color='tab:gray', lw=1.2, label='Requested Bus Power') - axes[2].plot(time_array, p_bus_kw, color='m', lw=1.4, label='Actual Bus Power') - axes[2].plot(time_array, p_shaft_kw, 'g-', lw=1.2, label='Shaft Power') - axes[2].plot(time_array, p_loss_kw, color='tab:orange', lw=1.0, label='Loss Power') - axes[2].set_ylabel('Power [kW]') - axes[2].set_title('Bus / Shaft / Loss Power') - axes[2].axhline(0.0, color='k', lw=0.6) - axes[2].grid(True, linestyle=':') - axes[2].legend() - - axes[3].plot(time_array, v_bus_log, color='tab:cyan', lw=1.4, label='Bus Voltage') - axes[3].plot(time_array, v_motor_log, color='tab:red', lw=1.2, label='Motor Potential') - axes[3].set_ylabel('Voltage [V]') - axes[3].set_title('Bus Voltage and Motor Equivalent Potential') - axes[3].grid(True, linestyle=':') - axes[3].legend() - - axes[4].plot(time_array, i_bus_log, color='tab:brown', lw=1.4, label='Bus Current') - axes[4].set_ylabel('Current [A]') - axes[4].set_xlabel('Time [s]') - axes[4].set_title('Bus Current') - axes[4].grid(True, linestyle=':') - axes[4].legend() - - plt.tight_layout() - plt.savefig("figures/test_complex_motor_output.png") - - return { - 'final_state': state, - 'peak_speed_rpm': float(np.max(n_log)), - 'peak_bus_current_a': float(np.max(i_bus_log)), - 'torque_balance_error_mean': float(np.mean(np.abs(torque_balance))), - 'power_balance_error_mean': float(np.mean(np.abs(power_balance))), - 'ise': ise, - 'iae': iae, - } - - def run_motor_battery_coupled_test() -> Dict[str, Any]: """执行混合动力电机与电池协同闭环抗扰动验证例程 - 复杂测试用例""" import matplotlib @@ -1019,12 +785,11 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]: P_rate=300e3, w_rate=575.95, J=0.8, - k_p_w=13.440362, - k_i_w=42.997816, - k_d_w=0.484660, P_const_loss=300.0, k_fe=4e-4, u_dc=battery.V_t, + mpc_W_speed=100, + mpc_W_dcost=12, ) dt = 0.02 @@ -1041,36 +806,60 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]: t_ext_log = [] t_load_log = [] + # 螺旋桨负载模型参数 + k_drag = 1.5e-6 # 气动阻力系数 (与转速平方成正比) + J_prop = 0.15 # 螺旋桨等效转动惯量 (kg·m²) + T_min = 10.0 # 最小阻力矩 (Nm) + p_actual_kw = 0.0 - for t in time_array: + n_prev = 0.0 # 上一时刻转速 (RPM) + w_prev = 0.0 # 上一时刻角速度 (rad/s) + for t in tqdm(time_array, desc="Coupled Test"): # 复杂测试工况 - 多段转速变化 + 多种扰动 p_ext_elec = 0.0 # 外部电功率扰动 (kW) - # 目标转速曲线 - 复杂多段变化 + # 目标转速曲线 - 包含阶跃、斜坡、正弦波等复杂信号 if t < 3.0: n_setpoint = 0.0 elif t < 8.0: - n_setpoint = 2000.0 + # 斜坡上升:从0到2000 RPM + n_setpoint = 2000.0 * (t - 3.0) / 5.0 elif t < 15.0: + # 阶跃 n_setpoint = 2800.0 elif t < 22.0: - n_setpoint = 2200.0 + # 正弦波动:2800 ± 600 RPM,周期10s + n_setpoint = 2800.0 + 600.0 * np.sin(2 * np.pi * (t - 15.0) / 10.0) elif t < 32.0: - n_setpoint = 3500.0 + # 斜坡上升 + 正弦波动 + base = 2200.0 + 1300.0 * (t - 22.0) / 10.0 # 斜坡上升 + n_setpoint = base + 300.0 * np.sin(2 * np.pi * (t - 22.0) / 8.0) elif t < 42.0: + # 阶跃到3800 n_setpoint = 3800.0 elif t < 52.0: - n_setpoint = 2800.0 + # 斜坡下降 + 正弦波动 + base = 3800.0 - 1000.0 * (t - 42.0) / 10.0 # 斜坡下降 + n_setpoint = base + 200.0 * np.sin(2 * np.pi * (t - 42.0) / 6.0) elif t < 62.0: - n_setpoint = 3000.0 + # 梯形波:先上升再保持 + if t < 55.0: + n_setpoint = 2800.0 + 400.0 * (t - 52.0) / 3.0 + else: + n_setpoint = 3200.0 elif t < 72.0: - n_setpoint = 4000.0 + # 正弦波动:3000 ± 1000 RPM + n_setpoint = 3000.0 + 1000.0 * np.sin(2 * np.pi * (t - 62.0) / 10.0) elif t < 82.0: - n_setpoint = 4200.0 + # 斜坡上升 + n_setpoint = 3000.0 + 600.0 * (t - 72.0) / 10.0 elif t < 92.0: - n_setpoint = 3200.0 + # 衰减正弦波 + amplitude = 400.0 * np.exp(-(t - 82.0) / 5.0) + n_setpoint = 3200.0 + amplitude * np.sin(2 * np.pi * (t - 82.0) / 8.0) else: - n_setpoint = 3000.0 + # 斜坡下降到3000 + n_setpoint = 3200.0 - 200.0 * (t - 92.0) / 8.0 # 外部转矩扰动 - 模拟发动机并联/涡轴输出变化 if t < 5.0: @@ -1096,38 +885,61 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]: else: t_ext = 0.0 - # 负载转矩 - 模拟风速/气压变化 - if t < 10.0: - t_load = 20.0 - elif t < 22.0: - t_load = 80.0 - elif t < 32.0: - t_load = 40.0 - elif t < 42.0: - t_load = 120.0 - elif t < 52.0: - t_load = 50.0 - elif t < 62.0: - t_load = 90.0 - elif t < 72.0: - t_load = 60.0 - elif t < 82.0: - t_load = 130.0 - else: - t_load = 70.0 + # 螺旋桨负载模型:气动阻力矩 + 惯性负载 + # 气动阻力: T = k_drag * n² (与转速平方成正比) + # 惯性负载: T = J_prop * alpha (与加速度成正比,模拟螺旋桨惯性) + n_current = n_setpoint # 使用目标转速计算负载 + w_current = n_current * 2 * np.pi / 60.0 # rad/s - # 外部电功率扰动 - if 35.0 < t < 40.0: + # 计算角加速度 (rad/s²) + alpha = (w_current - w_prev) / dt if dt > 0 else 0.0 + + # 气动阻力矩 (与转速平方成正比) + t_aero = k_drag * n_current ** 2 + T_min + # 惯性负载 (与加速度成正比) + t_inertia = J_prop * alpha + + t_load = t_aero + t_inertia + + # 更新上一时刻转速 + w_prev = w_current + + # 外部电功率扰动 (发动机/燃气轮机发电功率注入,正值表示充电,负值表示负载) + if 5.0 < t < 10.0: + p_ext_elec = 40.0 # 涡轴发电机注入 40kW + elif 10.0 < t < 15.0: + p_ext_elec = 50.0 # 涡轴发电机注入 50kW + elif 20.0 < t < 23.0: + p_ext_elec = 60.0 # 涡轴发电机注入 60kW + elif 25.0 < t < 30.0: + p_ext_elec = 70.0 # 涡轴发电机注入 70kW + elif 32.0 < t < 35.0: + p_ext_elec = 55.0 # 涡轴发电机注入 55kW + elif 35.0 < t < 40.0: p_ext_elec = 80.0 # 涡轴发电机注入 80kW + elif 45.0 < t < 48.0: + p_ext_elec = 90.0 # 涡轴发电机注入 90kW (短时高功率充电) + elif 50.0 < t < 53.0: + p_ext_elec = 45.0 # 涡轴发电机注入 45kW elif 55.0 < t < 60.0: - p_ext_elec = -100.0 # 其他设备用电 100kW + p_ext_elec = -100.0 # 其他设备用电 100kW (负载) + elif 62.0 < t < 65.0: + p_ext_elec = 50.0 # 涡轴发电机注入 50kW + elif 65.0 < t < 70.0: + p_ext_elec = 65.0 # 涡轴发电机注入 65kW + elif 72.0 < t < 75.0: + p_ext_elec = 55.0 # 涡轴发电机注入 55kW elif 75.0 < t < 80.0: p_ext_elec = 60.0 # 涡轴发电机注入 60kW + elif 82.0 < t < 85.0: + p_ext_elec = 70.0 # 涡轴发电机注入 70kW + elif 88.0 < t < 95.0: + p_ext_elec = 75.0 # 涡轴发电机注入 75kW # 母线电压波动 v_bus = battery.V_t + 15.0 * np.sin(2 * np.pi * t / 8.0) + 5.0 * np.sin(2 * np.pi * t / 3.0) - # 延迟一拍的实际电能参与当前动态步,进而PID在内环打出对下一拍的请求 + # 延迟一拍的实际电能参与当前动态步,进而MPC在内环打出对下一拍的请求 state = motor.step(dt, n_setpoint, p_actual_kw, v_bus, t_load, t_ext) p_req_kw = state['p_bus_req_kw'] @@ -1174,7 +986,7 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]: axes[0].legend() # 2. 转矩响应 - axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque (PID Output)') + axes[1].plot(time_array, t_motor_log, 'r-', lw=1.6, label='Motor Torque (MPC Output)') axes[1].plot(time_array, np.array(t_load_log), 'k--', lw=1.2, label='Propeller Load Torque') axes[1].plot(time_array, np.array(t_ext_log), color='tab:purple', lw=1.2, label='External Torque') axes[1].axhline(0.0, color='k', lw=0.6) @@ -1210,7 +1022,7 @@ def run_motor_battery_coupled_test() -> Dict[str, Any]: axes[4].legend() plt.tight_layout() - plt.savefig("figures/test_complex_output.png") + plt.savefig("figures/test_complex_output.png", dpi=300, bbox_inches='tight') print("Plot saved to figures/test_complex_output.png") return { diff --git a/Model/src/mpc_controller.py b/Model/src/mpc_controller.py new file mode 100644 index 0000000..8118740 --- /dev/null +++ b/Model/src/mpc_controller.py @@ -0,0 +1,298 @@ +import numpy as np + + +# ============================================================================== +# Pure-Python projected gradient descent for box-constrained optimization +# (替代 scipy.optimize.minimize SLSQP,避免 Fortran ABI 兼容性问题) +# ============================================================================== +def _minimize_box(objective, x0, bounds, lr=1.0, max_iter=80, ftol=1e-6): + """ + Projected gradient descent with Armijo backtracking line search. + For small-to-medium MPC horizons (H ≤ 30) this is fast enough. + """ + n = len(x0) + lb = np.array([b[0] for b in bounds], dtype=np.float64) + ub = np.array([b[1] for b in bounds], dtype=np.float64) + x = np.clip(np.array(x0, dtype=np.float64), lb, ub) + + eps = 1e-5 # finite-difference step + f_prev = objective(x) + + for _it in range(max_iter): + # Approximate gradient via central differences + grad = np.empty(n, dtype=np.float64) + for i in range(n): + x_p = x.copy(); x_p[i] += eps + x_m = x.copy(); x_m[i] -= eps + grad[i] = (objective(x_p) - objective(x_m)) / (2 * eps) + + # Backtracking line search (Armijo condition) + step = lr + for _ in range(12): + x_new = np.clip(x - step * grad, lb, ub) + f_new = objective(x_new) + if f_new < f_prev - 1e-4 * step * np.dot(grad, x - x_new): + break + step *= 0.5 + else: + x_new = np.clip(x - step * grad, lb, ub) + f_new = objective(x_new) + + if abs(f_prev - f_new) < ftol: + x = x_new + break + x = x_new + f_prev = f_new + + class _Result: + pass + res = _Result() + res.x = x + res.fun = f_prev + res.success = True + return res + +# ============================================================================== +# 1. 涡轴发动机 MPC 控制器 (基于 Scipy SLSQP 高速求解) +# ============================================================================== +class TurboShaftMPCController: + def __init__(self, tau_fuel, K_inertia, dt=0.02, horizon=15, + min_fuel=10.0, max_fuel=600.0, overshoot_limit=0.05): + """ + 初始化高速 MPC 控制器 (基于 Scipy SLSQP) + 替代 GEKKO 以消除文件 I/O 开销,提升单步推理速度 50x 以上。 + + :param overshoot_limit: 超调量硬约束 (0.05 = 5%) + """ + self.dt = dt + self.H = horizon + self.tau_fuel = tau_fuel + self.K_inertia = K_inertia + self.min_fuel = min_fuel + self.max_fuel = max_fuel + + # 权重参数 (与 GEKKO 版本保持一致) + self.W_power = 200.0 # Power Setpoint Weight + self.W_dcost = 1.5 # Delta Control Weight (DCOST) + + # 超调量约束 + self.overshoot_limit = overshoot_limit # 5% 硬约束 + + # 超调惩罚权重 (使用很大的值使约束"硬"化) + self.W_overshoot_penalty = 1e6 + + # 状态缓存 + self.last_u = 100.0 + self.last_N = 0.0 + + def reset(self, initial_output, initial_N): + self.last_u = initial_output + self.last_N = initial_N + + def compute(self, current_N, current_Wfact, target_power, engine_model=None, H_env=0, Ma_env=0, precalc_params=None, **kwargs): + """ + 计算 MPC 控制律 + :param precalc_params: (必须) 元组 (Wf_req_0, Power_0, k_wf, k_p)。 + FastMPC 必须配合 Batch Prediction 使用。 + """ + if precalc_params is not None: + Wf_req_0, Power_0, k_wf, k_p = precalc_params + else: + raise ValueError("FastMPC 必须配合 Batch Prediction 使用 (提供 precalc_params)") + + N0 = current_N + Wf_act0 = current_Wfact + N_ref = N0 # 用于线性化的参考点 + + # --- 构建线性预测模型 --- + # State x = [N, Wf_act] + # x_{k+1} = A x_k + B u_k + d + # N_{k+1} = N_k + dt * K * (Wf_act_k - (Wf_req_0 + k_wf*(N_k - N0))) + # = (1 - dt*K*k_wf) N_k + (dt*K) Wf_act_k + dt*K*(k_wf*N0 - Wf_req_0) + # Wf_act_{k+1} = Wf_act_k + dt * (u_k - Wf_act_k) / tau + # = (1 - dt/tau) Wf_act_k + (dt/tau) u_k + + dt = self.dt + K = self.K_inertia + tau = self.tau_fuel + + A = np.array([ + [1 - dt * K * k_wf, dt * K], + [0, 1 - dt / tau] + ]) + B = np.array([0, dt / tau]) + d = np.array([dt * K * (k_wf * N_ref - Wf_req_0), 0]) + + # Power Output: P = P0 + k_p * (N - N0) + # = k_p * N + (P0 - k_p * N0) + C_p = k_p + D_p = Power_0 - k_p * N_ref + + # --- 超调量硬约束 --- + # 升功率时: P_k <= target_power * (1 + overshoot_limit) + # 降功率时: P_k >= target_power * (1 - overshoot_limit) + P_max = target_power * (1 + self.overshoot_limit) + P_min = target_power * (1 - self.overshoot_limit) + + # 判断是升功率还是降功率 + is_ramping_up = target_power > Power_0 + + # --- 优化目标函数 --- + # Variables: U = [u_0, ..., u_{H-1}] + # x_0 is fixed. + # Cost = sum_{k=1 to H} W_power * (P_k - P_target)^2 + sum_{k=0 to H-1} W_dcost * (u_k - u_{k-1})^2 + # + W_overshoot_penalty * (违反约束的惩罚) + + def objective(U): + cost = 0.0 + x_k = np.array([N0, Wf_act0]) + u_prev = self.last_u # Use last commanded u for the first delta + + for k in range(self.H): + u_k = U[k] + + # Dynamics Step + x_k = A @ x_k + B * u_k + d + + # Output + P_k = C_p * x_k[0] + D_p + + # Cost Accumulation + cost += self.W_power * (P_k - target_power) ** 2 + cost += self.W_dcost * (u_k - u_prev) ** 2 + + # 超调惩罚: 如果违反约束,添加巨大惩罚 + if is_ramping_up: + if P_k > P_max: + cost += self.W_overshoot_penalty * (P_k - P_max) ** 2 + else: + if P_k < P_min: + cost += self.W_overshoot_penalty * (P_min - P_k) ** 2 + + u_prev = u_k + + return cost + + # --- 求解 --- + # 初始猜测: 保持上一次的输入 + U0 = np.full(self.H, float(self.last_u), dtype=np.float64) + + # 约束: lb <= u <= ub + bounds = [(self.min_fuel, self.max_fuel) for _ in range(self.H)] + + # 使用纯Python投影梯度下降求解带约束优化 (替代 SLSQP) + res = _minimize_box(objective, U0, bounds, lr=5.0, max_iter=60, ftol=1e-3) + + # 更新状态 + u_opt = res.x[0] + self.last_u = u_opt + + return u_opt + + +# ============================================================================== +# 2. 驱动电机 MPC 控制器 (基于 Scipy SLSQP 高速求解) +# ============================================================================== +class MotorMPCController: + def __init__(self, J, B_visc, dt=0.02, horizon=10, W_speed=100.0, W_dcost=1.0, overshoot_limit=0.05): + """ + 初始化高速电机 MPC 控制器 (基于 Scipy SLSQP) + 替代 GEKKO 以消除文件 I/O 开销,提升单步推理速度 50x 以上。 + + :param overshoot_limit: 超调量硬约束 (0.05 = 5%) + """ + self.dt = dt + self.H = horizon + self.J = J + self.B_visc = B_visc + + # 权重参数 (可调) + self.W_speed = W_speed # 转速跟踪权重 + self.W_dcost = W_dcost # 控制变化惩罚 + + # 超调量约束 + self.overshoot_limit = overshoot_limit # 5% 硬约束 + + # 超调惩罚权重 (使用很大的值使约束"硬"化) + self.W_overshoot_penalty = 1e6 + + # 状态缓存 + self.last_T_cmd = 0.0 + + def reset(self, initial_w=0.0): + """重新初始化状态""" + self.last_T_cmd = 0.0 + + def compute(self, current_w, target_w, t_load, t_ext, t_lim_upper, t_lim_lower): + """ + 执行单步 MPC 优化计算 + :param current_w: 当前实际转速 (rad/s) + :param target_w: 目标转速 (rad/s) + :param t_load: 当前气动负载转矩 (Nm) + :param t_ext: 当前外部轴系转矩 (Nm) + :param t_lim_upper: 当前母线电压下,电机能发出的【正向转矩上限】(发电极限) + :param t_lim_lower: 当前母线电压下,电机能发出的【负向转矩下限】(驱动极限) + :return: 最优转矩指令 (Nm) + """ + w0 = current_w + + dt = self.dt + J = self.J + B = self.B_visc + + # --- 离散线性模型 --- + # w_{k+1} = w_k + dt * (-(T_cmd + T_ext + T_load + B*w_k) / J) + # = (1 - dt*B/J) * w_k - dt/J * T_cmd - dt/J * (T_ext + T_load) + a = 1 - dt * B / J + b = -dt / J + d = -dt / J * (t_ext + t_load) + + # --- 超调量硬约束 --- + # 升速时: w_k <= target_w * (1 + overshoot_limit) + # 降速时: w_k >= target_w * (1 - overshoot_limit) + w_max = target_w * (1 + self.overshoot_limit) + w_min = target_w * (1 - self.overshoot_limit) + + # 判断是升速还是降速 + is_ramping_up = target_w > w0 + + # --- 优化目标函数 --- + # Cost = sum_{k=1 to H} W_speed * (w_k - target_w)^2 + sum_{k=0 to H-1} W_dcost * (T_k - T_{k-1})^2 + # + W_overshoot_penalty * (违反约束的惩罚) + + def objective(U): + cost = 0.0 + w_k = w0 + T_prev = self.last_T_cmd + + for k in range(self.H): + T_k = U[k] + w_k = a * w_k + b * T_k + d # 动力学迭代 + cost += self.W_speed * (w_k - target_w) ** 2 + cost += self.W_dcost * (T_k - T_prev) ** 2 + + # 超调惩罚: 如果违反约束,添加巨大惩罚 + if is_ramping_up: + if w_k > w_max: + cost += self.W_overshoot_penalty * (w_k - w_max) ** 2 + else: + if w_k < w_min: + cost += self.W_overshoot_penalty * (w_min - w_k) ** 2 + + T_prev = T_k + + return cost + + # 初始猜测 + U0 = np.full(self.H, float(self.last_T_cmd), dtype=np.float64) + + # 约束: 转矩边界 + bounds = [(t_lim_lower, t_lim_upper) for _ in range(self.H)] + + # 求解 (使用纯Python投影梯度下降,替代 SLSQP) + res = _minimize_box(objective, U0, bounds, lr=50.0, max_iter=120, ftol=1e-5) + + T_opt = res.x[0] + self.last_T_cmd = T_opt + + return T_opt diff --git a/Model/src/series_hybrid_sim.py b/Model/src/series_hybrid_sim.py index 7071cd1..17ce25d 100644 --- a/Model/src/series_hybrid_sim.py +++ b/Model/src/series_hybrid_sim.py @@ -1,6 +1,6 @@ -import os import numpy as np import matplotlib.pyplot as plt +from tqdm import tqdm from engine_dynamic_sim import TurboshaftDynamicSim from motor_sim import MotorSim @@ -10,25 +10,15 @@ class SeriesHybridSystem: """ 串联式混合动力系统总成 """ - def __init__(self): - # ===== 新增:按Model目录定位GPR数据与权重,避免从主项目调用时路径错误 ===== - model_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) - gpr_csv_path = os.path.join(model_root, "data", "Cleaned_Engine_Data_Full.csv") - gpr_pth_path = os.path.join(model_root, "data", "engine_gpr_model.pth") - self.genset = TurboshaftDynamicSim( - gpr_csv_path=gpr_csv_path, - gpr_pth_path=gpr_pth_path, - kp=0.5, - ki=0.5, - kd=0.0 - ) + def __init__(self, mpc_overshoot_limit: float = 0.05): + self.genset = TurboshaftDynamicSim(mpc_overshoot_limit=mpc_overshoot_limit) self.drive_motor = MotorSim( P_rate=300e3, w_rate=575.95, - k_p_w=5.0, # 增大比例增益 - k_i_w=2.0, # 增大积分增益 - k_d_w=0.5, # 适度微分 J=1.0, + mpc_W_speed=40, + mpc_W_dcost=5, + mpc_overshoot_limit=mpc_overshoot_limit, # 超调量硬约束 (5%) ) self.battery = BatterySim(capacity_kwh=50.0, initial_soc=0.6) @@ -172,7 +162,7 @@ if __name__ == "__main__": 'p_batt_actual_kw', 'wf_kg_h']} print("-> 开始全系统闭环步进仿真 (总时长 3 分钟)...") - for t in time_array: + for t in tqdm(time_array, desc='Simulating', unit='step'): # 3分钟测试剖面 if t < 15.0: target_rpm, load_torque = 1500.0, 50.0 # 地面滑行 @@ -204,7 +194,7 @@ if __name__ == "__main__": # 2. 功率分配流向 axes[1].plot(time_array, log['p_drive_req_kw'], 'k--', lw=1.5, label='Drive Motor Request') - axes[1].plot(time_array, log['p_engine_out_kw'], 'r-', lw=1.5, label='Engine Output (APU)') + axes[1].plot(time_array, log['p_engine_out_kw'], 'r-', lw=1.5, label='Engine Output (Turbo Shaft)') axes[1].plot(time_array, log['p_batt_actual_kw'], 'g-', lw=1.5, label='Battery Output') axes[1].set_ylabel('Power [kW]') axes[1].set_title('System Power Flow (Energy Management)') @@ -249,7 +239,7 @@ if __name__ == "__main__": os.makedirs(data_dir) dat_path = os.path.join(data_dir, 'series_hybrid_data.dat') - with open(dat_path, 'w') as f: + with open(dat_path, 'w', encoding='utf-8') as f: f.write('# Time(s)\tTarget_RPM\tActual_RPM\tError_RPM\tSOC\tEngine_Power\tDrive_Req\tBatt_Power\n') for i in range(len(time_array)): err = log['target_prop_rpm'][i] - log['prop_speed_rpm'][i] diff --git a/app.py b/app.py index 976d8ef..2506725 100644 --- a/app.py +++ b/app.py @@ -1,3 +1,4 @@ +import os import gradio as gr import time from functools import partial @@ -10,8 +11,8 @@ from analysis_functions import ( frequency_domain_analysis, root_locus_analysis ) -# ===== 新增:算例演示模块函数导入 ===== -from case_demo_functions import run_case_demo +# ===== 算例演示模块函数导入(四阶段设计)===== +from case_demo_functions import run_distillation_demo, run_gpr_training, run_engine_design, run_motor_design, run_hybrid_demo from chatbot import chat_with_ai from user_stats import get_online_status_html, update_user_activity from ui_components import ( @@ -23,8 +24,98 @@ from ui_components import ( create_chatbot_tab ) + +# ===== 系统资源监控 ===== +def get_system_monitor_html(): + """获取 CPU / 内存 / GPU 使用率的 HTML 小组件""" + try: + import psutil + cpu_pct = psutil.cpu_percent(interval=0) + mem = psutil.virtual_memory() + mem_pct = mem.percent + mem_used_gb = mem.used / (1024 ** 3) + mem_total_gb = mem.total / (1024 ** 3) + except ImportError: + return "
psutil 未安装,无法监控系统资源
" + + # GPU 信息 — 优先用 nvidia-smi(不依赖 PyTorch CUDA 版本),再用 torch.cuda 兜底 + gpu_html = "" + try: + import subprocess as _sp + _r = _sp.run( + ['nvidia-smi', '--query-gpu=name,memory.used,memory.total,utilization.gpu', + '--format=csv,noheader,nounits'], + capture_output=True, text=True, timeout=3 + ) + if _r.returncode == 0 and _r.stdout.strip(): + _parts = [p.strip() for p in _r.stdout.strip().split('\n')[0].split(',')] + _gpu_mem_used = float(_parts[1]) / 1024 # MiB → GiB + _gpu_mem_total = float(_parts[2]) / 1024 + _gpu_util = float(_parts[3]) + _gc = '#ff6b6b' if _gpu_util > 80 else '#ffd93d' if _gpu_util > 50 else '#6bcb77' + gpu_html = ( + f"
" + f"🎮 GPU" + f"
" + f"
" + f"
" + f"{_gpu_util:.0f}%   {_gpu_mem_used:.1f}/{_gpu_mem_total:.0f} GB" + f"
" + ) + else: + raise RuntimeError("nvidia-smi no output") + except Exception: + try: + import torch as _torch + if _torch.cuda.is_available(): + _mem_alloc = _torch.cuda.memory_allocated(0) / (1024 ** 3) + _mem_total = _torch.cuda.get_device_properties(0).total_memory / (1024 ** 3) + _util = _mem_alloc / max(_mem_total, 0.01) * 100 + _gc = '#ff6b6b' if _util > 80 else '#ffd93d' if _util > 50 else '#6bcb77' + gpu_html = ( + f"
" + f"🎮 GPU" + f"
" + f"
" + f"
" + f"{_mem_alloc:.1f}/{_mem_total:.0f} GB" + f"
" + ) + else: + gpu_html = "
🎮 GPU N/A
" + except Exception: + gpu_html = "
🎮 GPU N/A
" + + cpu_color = '#ff6b6b' if cpu_pct > 80 else '#ffd93d' if cpu_pct > 50 else '#6bcb77' + mem_color = '#ff6b6b' if mem_pct > 80 else '#ffd93d' if mem_pct > 50 else '#6bcb77' + + html = ( + f"
" + # CPU + f"
" + f"🖥️ CPU" + f"
" + f"
" + f"
" + f"{cpu_pct:.0f}%" + f"
" + # Memory + f"
" + f"💾 RAM" + f"
" + f"
" + f"
" + f"{mem_used_gb:.1f}/{mem_total_gb:.0f} GB ({mem_pct:.0f}%)" + f"
" + # GPU + f"{gpu_html}" + f"
" + ) + return html + # 加载外部CSS文件 -with open("assets/styles.css", "r", encoding="utf-8") as f: +with open(os.path.join(os.path.dirname(__file__), "assets", "styles.css"), "r", encoding="utf-8") as f: custom_css = f.read() # --- 主应用界面 --- @@ -35,24 +126,9 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus # 创建头部信息和在线计数器 online_counter = create_header() - - # 创建共享的输入组件 - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(): - gr.HTML("
📊 通用系统参数
") - num_input = gr.Textbox( - label="传递函数分子系数 (Numerator)", - value="1", - placeholder="例如: 1 或 1,2,3", - info="💡 用逗号分隔多个系数,从最高次项到常数项" - ) - den_input = gr.Textbox( - label="传递函数分母系数 (Denominator)", - value="1,6,11,6", - placeholder="例如: 1,2,1", - info="💡 分母阶数通常高于或等于分子阶数" - ) + + # 系统资源监控(始终可见) + system_monitor = gr.HTML(value=get_system_monitor_html, elem_id="system-monitor") # 创建功能选项卡 with gr.Tabs() as tabs: @@ -80,13 +156,13 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus # --- 时域分析事件 --- time_domain_ui["confirm_button"].click( fn=display_transfer_function, - inputs=[num_input, den_input], + inputs=[time_domain_ui["num_input"], time_domain_ui["den_input"]], outputs=[time_domain_ui["tf_display"]] ).then(lambda: get_online_status_html(), outputs=online_counter) time_domain_ui["analyze_button"].click( fn=time_domain_analysis, - inputs=[num_input, den_input], + inputs=[time_domain_ui["num_input"], time_domain_ui["den_input"]], outputs=[time_domain_ui["output_plot"], time_domain_ui["output_metrics"]] ).then(lambda: get_online_status_html(), outputs=online_counter) @@ -96,7 +172,7 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus fig, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k) return fig, metrics, tf_latex, stability, k, get_online_status_html() - freq_inputs = [num_input, den_input, freq_domain_ui["log_k_slider"]] + freq_inputs = [freq_domain_ui["num_input"], freq_domain_ui["den_input"], freq_domain_ui["log_k_slider"]] freq_outputs = [ freq_domain_ui["plot_output"], freq_domain_ui["metrics_display"], @@ -116,7 +192,7 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus fig, poles, k_val = root_locus_analysis(num, den, log_k) return fig, poles, k_val, get_online_status_html() - rl_inputs = [root_locus_ui["log_k_slider"], num_input, den_input] + rl_inputs = [root_locus_ui["log_k_slider"], root_locus_ui["num_input"], root_locus_ui["den_input"]] rl_outputs = [ root_locus_ui["plot_output"], root_locus_ui["poles_display"], @@ -129,59 +205,170 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus outputs=rl_outputs ) - # 当输入框变化时,也更新频域和根轨迹(如果它们是当前可见的) - def update_all_on_tf_change(num, den, log_k_freq, log_k_rl): - # 更新频域 - k_freq = 10**log_k_freq - fig_freq, metrics, tf_latex, stability = frequency_domain_analysis(num, den, k_freq) - - # 更新根轨迹 - fig_rl, poles, k_val_rl = root_locus_analysis(num, den, log_k_rl) + # 频域:当传递函数输入框变化时自动更新 + freq_domain_ui["num_input"].change( + fn=update_frequency_analysis_wrapper, + inputs=freq_inputs, outputs=freq_outputs + ) + freq_domain_ui["den_input"].change( + fn=update_frequency_analysis_wrapper, + inputs=freq_inputs, outputs=freq_outputs + ) - return ( - fig_freq, metrics, tf_latex, stability, k_freq, - fig_rl, poles, k_val_rl, - get_online_status_html() - ) + # 根轨迹:当传递函数输入框变化时自动更新 + root_locus_ui["num_input"].change( + fn=update_rl_view_wrapper, + inputs=rl_inputs, outputs=rl_outputs + ) + root_locus_ui["den_input"].change( + fn=update_rl_view_wrapper, + inputs=rl_inputs, outputs=rl_outputs + ) - tf_change_inputs = [num_input, den_input, freq_domain_ui["log_k_slider"], root_locus_ui["log_k_slider"]] - tf_change_outputs = freq_outputs[:-1] + rl_outputs[:-1] + [online_counter] - - num_input.change(fn=update_all_on_tf_change, inputs=tf_change_inputs, outputs=tf_change_outputs) - den_input.change(fn=update_all_on_tf_change, inputs=tf_change_inputs, outputs=tf_change_outputs) + # ===== 算例演示事件绑定(四阶段)===== - # ===== 新增:算例演示事件包装器 ===== - def run_case_demo_wrapper(sim_time, dt, initial_soc, initial_engine_power, profile, rpm_scale, load_scale, sid): + # --- 阶段零-A:GPR 模型训练 --- + def run_gpr_wrapper(mode, sid, progress=gr.Progress(track_tqdm=True)): update_user_activity(sid) - fig, summary, table_data = run_case_demo( - sim_time_s=sim_time, - dt=dt, - initial_soc_pct=initial_soc, - initial_engine_power_kw=initial_engine_power, - profile_name=profile, - rpm_scale=rpm_scale, - load_scale=load_scale + fig, summary = run_gpr_training(mode=mode, progress=progress) + return fig, summary, get_online_status_html() + + case_demo_ui["gpr_run_button"].click( + fn=run_gpr_wrapper, + inputs=[case_demo_ui["gpr_mode"], session_id], + outputs=[case_demo_ui["gpr_plot"], case_demo_ui["gpr_summary"], online_counter] + ) + + # --- 阶段零-B:NN 模型训练(蒸馏)--- + def run_distillation_wrapper(epochs, lr, hidden, sid, progress=gr.Progress(track_tqdm=True)): + update_user_activity(sid) + fig, summary = run_distillation_demo(epochs, lr, hidden, progress=progress) + return fig, summary, get_online_status_html() + + case_demo_ui["distill_run_button"].click( + fn=run_distillation_wrapper, + inputs=[ + case_demo_ui["distill_epochs"], case_demo_ui["distill_lr"], + case_demo_ui["distill_hidden"], + session_id + ], + outputs=[case_demo_ui["distill_plot"], case_demo_ui["distill_summary"], online_counter] + ) + + # --- 阶段一:发动机控制器设计 --- + def run_engine_design_wrapper(sim_time, dt, init_power, target_power, + controller_type, + kp, ki, kd, tau_fuel, K_inertia, + mpc_horizon, mpc_W_power, mpc_W_dcost, mpc_overshoot, + sid, progress=gr.Progress(track_tqdm=True)): + update_user_activity(sid) + fig, summary = run_engine_design( + sim_time, dt, init_power, target_power, + controller_type, + kp, ki, kd, tau_fuel, K_inertia, + mpc_horizon, mpc_W_power, mpc_W_dcost, mpc_overshoot / 100.0, + progress=progress + ) + return fig, summary, get_online_status_html() + + case_demo_ui["eng_run_button"].click( + fn=run_engine_design_wrapper, + inputs=[ + case_demo_ui["eng_sim_time"], case_demo_ui["eng_dt"], + case_demo_ui["eng_init_power"], case_demo_ui["eng_target_power"], + case_demo_ui["eng_controller_type"], + case_demo_ui["eng_kp"], case_demo_ui["eng_ki"], case_demo_ui["eng_kd"], + case_demo_ui["eng_tau_fuel"], case_demo_ui["eng_K_inertia"], + case_demo_ui["eng_mpc_horizon"], case_demo_ui["eng_mpc_W_power"], + case_demo_ui["eng_mpc_W_dcost"], case_demo_ui["eng_mpc_overshoot"], + session_id + ], + outputs=[case_demo_ui["eng_plot"], case_demo_ui["eng_summary"], online_counter] + ) + + # --- 阶段二:电机控制器设计 --- + def run_motor_design_wrapper(sim_time, dt, target_rpm, load_torque, + controller_type, + kp, ki, kd, J, + mpc_W_speed, mpc_W_dcost, mpc_overshoot, + sid, progress=gr.Progress(track_tqdm=True)): + update_user_activity(sid) + fig, summary = run_motor_design( + sim_time, dt, target_rpm, load_torque, + controller_type, + kp, ki, kd, J, + mpc_W_speed, mpc_W_dcost, mpc_overshoot / 100.0, + progress=progress + ) + return fig, summary, get_online_status_html() + + case_demo_ui["mot_run_button"].click( + fn=run_motor_design_wrapper, + inputs=[ + case_demo_ui["mot_sim_time"], case_demo_ui["mot_dt"], + case_demo_ui["mot_target_rpm"], case_demo_ui["mot_load_torque"], + case_demo_ui["mot_controller_type"], + case_demo_ui["mot_kp"], case_demo_ui["mot_ki"], case_demo_ui["mot_kd"], + case_demo_ui["mot_J"], + case_demo_ui["mot_mpc_W_speed"], case_demo_ui["mot_mpc_W_dcost"], + case_demo_ui["mot_mpc_overshoot"], + session_id + ], + outputs=[case_demo_ui["mot_plot"], case_demo_ui["mot_summary"], online_counter] + ) + + # --- 阶段三:能量管理策略设计(自动引用前两阶段控制器参数)--- + def run_hybrid_demo_wrapper(sim_time, dt, initial_soc, initial_engine_power, + profile, + eng_ctrl_type, eng_kp, eng_ki, eng_kd, + eng_mpc_horizon, eng_mpc_W_power, eng_mpc_W_dcost, eng_mpc_overshoot, + mot_ctrl_type, mot_kp, mot_ki, mot_kd, mot_J, + mot_mpc_W_speed, mot_mpc_W_dcost, mot_mpc_overshoot, + soc_target, soc_low, soc_high, + p_eng_min, p_eng_max, p_charge, k_soc, + power_reserve, battery_capacity, sid, + progress=gr.Progress(track_tqdm=True)): + update_user_activity(sid) + fig, summary, table_data = run_hybrid_demo( + sim_time, dt, initial_soc, initial_engine_power, profile, + eng_ctrl_type, eng_kp, eng_ki, eng_kd, + eng_mpc_horizon, eng_mpc_W_power, eng_mpc_W_dcost, eng_mpc_overshoot / 100.0, + mot_ctrl_type, mot_kp, mot_ki, mot_kd, mot_J, + mot_mpc_W_speed, mot_mpc_W_dcost, mot_mpc_overshoot / 100.0, + soc_target, soc_low, soc_high, + p_eng_min, p_eng_max, p_charge, k_soc, + power_reserve, battery_capacity, + progress=progress ) return fig, summary, table_data, get_online_status_html() - # ===== 新增:算例演示按钮事件绑定 ===== - case_demo_ui["run_button"].click( - fn=run_case_demo_wrapper, + case_demo_ui["hybrid_run_button"].click( + fn=run_hybrid_demo_wrapper, inputs=[ - case_demo_ui["sim_time"], - case_demo_ui["dt"], - case_demo_ui["initial_soc"], - case_demo_ui["initial_engine_power"], + case_demo_ui["sim_time"], case_demo_ui["dt"], + case_demo_ui["initial_soc"], case_demo_ui["initial_engine_power"], case_demo_ui["profile"], - case_demo_ui["rpm_scale"], - case_demo_ui["load_scale"], + # 发动机控制器参数 + case_demo_ui["eng_controller_type"], + case_demo_ui["eng_kp"], case_demo_ui["eng_ki"], case_demo_ui["eng_kd"], + case_demo_ui["eng_mpc_horizon"], case_demo_ui["eng_mpc_W_power"], + case_demo_ui["eng_mpc_W_dcost"], case_demo_ui["eng_mpc_overshoot"], + # 电机控制器参数 + case_demo_ui["mot_controller_type"], + case_demo_ui["mot_kp"], case_demo_ui["mot_ki"], case_demo_ui["mot_kd"], + case_demo_ui["mot_J"], + case_demo_ui["mot_mpc_W_speed"], case_demo_ui["mot_mpc_W_dcost"], + case_demo_ui["mot_mpc_overshoot"], + # 能量管理策略参数 + case_demo_ui["soc_target"], case_demo_ui["soc_low"], case_demo_ui["soc_high"], + case_demo_ui["p_eng_min"], case_demo_ui["p_eng_max"], + case_demo_ui["p_charge"], case_demo_ui["k_soc"], + case_demo_ui["power_reserve"], case_demo_ui["battery_capacity"], session_id ], outputs=[ - case_demo_ui["plot"], - case_demo_ui["summary"], - case_demo_ui["table"], - online_counter + case_demo_ui["hybrid_plot"], case_demo_ui["hybrid_summary"], + case_demo_ui["hybrid_table"], online_counter ] ) @@ -215,14 +402,13 @@ with gr.Blocks(title="自动控制理论学习网站 - AI+数智平台", css=cus ) # --- 页面加载和定时器事件 --- - def on_page_load(sid): - update_user_activity(sid) - return get_online_status_html() - - demo.load(fn=on_page_load, inputs=[session_id], outputs=[online_counter]) + demo.load(fn=lambda: get_online_status_html(), outputs=[online_counter]) gr.Timer(10).tick(fn=get_online_status_html, outputs=online_counter) + # 系统资源监控定时刷新(每 3 秒) + gr.Timer(3).tick(fn=get_system_monitor_html, outputs=system_monitor) + if __name__ == "__main__": demo.queue().launch( diff --git a/assets/knowledge_cards_html.py b/assets/knowledge_cards_html.py index eba761e..cc19ef3 100644 --- a/assets/knowledge_cards_html.py +++ b/assets/knowledge_cards_html.py @@ -1,5 +1,5 @@ """ -知识卡片内容 - 包含时域、频域、根轨迹分析的公式和方法 +知识卡片内容 - 包含时域、频域、根轨迹、控制器设计、模型蒸馏、能量管理分析的公式和方法 使用纯HTML格式,无需LaTeX渲染库 """ @@ -497,3 +497,1676 @@ ROOT_LOCUS_KNOWLEDGE = """ """ + + +# ============================================================ +# 发动机控制器设计知识卡片 (PID + MPC) — 中文 +# ============================================================ +ENGINE_CONTROL_KNOWLEDGE = """ +
+

+ 🔧 发动机控制器设计理论 +

+ +
+ + 🎛️ PID 控制器 — 增量式形式 + +
+

标准PID传递函数:

+
+
+ u(t) = Kp e(t) + + Kie(τ)dτ + + Kd + + de(t) + dt + +
+
+

增量式(速度型)PID:

+
+
+ Δu(k) = Kp[e(k) − e(k−1)] + + Ki e(k) + + Kd[e(k) − 2e(k−1) + e(k−2)] +
+
+
+

💡 增量式优势:

+
    +
  • 输出为控制量的增量 Δu,而非绝对值,避免积分饱和(Windup)
  • +
  • 手动/自动切换时输出无跳变,适合工程实际
  • +
  • 计算量恒定,不随运行时间增加
  • +
  • 即使计算失误,影响仅限于当前步的增量
  • +
+
+ +

📊 各参数作用与调节规律

+ + + + + + + + + + + + + + + + + + + + + + + + + +
参数增大效果过大时调节建议
Kp (比例)加快响应速度,减小稳态误差产生振荡甚至不稳定先调大至出现振荡,再回退70%
Ki (积分)彻底消除稳态误差超调加重,积分饱和从小值开始缓慢增大
Kd (微分)抑制超调和振荡,改善动态品质放大噪声,响应迟钝通常取较小值,有噪声时可设为0
+ +
+

🎯 经典调参步骤(Ziegler-Nichols 启发式):

+
    +
  1. 设 Ki=0, Kd=0,逐渐增大 Kp 直到系统出现等幅振荡
  2. +
  3. 记录此时的临界增益 Ku 和振荡周期 Tu
  4. +
  5. 按经验公式设定:Kp=0.6Ku, Ki=2Kp/Tu, Kd=KpTu/8
  6. +
  7. 根据实际响应微调各参数
  8. +
+
+
+
+ +
+ + 🧠 MPC — 模型预测控制 + +
+

MPC(Model Predictive Control)利用系统的内部动态模型预测未来行为, + 在滚动时域上优化控制序列,仅执行第一步,然后重新优化——这就是"滚动优化"原理。

+ +

🔑 优化目标函数

+
+
+ minU   J = Σk=1H + [ Wp (PkPref)² + + WΔuuk)² ] +
+
+

+ 其中 H 为预测时域长度,Wp 为功率跟踪权重,WΔu 为控制增量惩罚权重。 +

+ +

🔑 MPC 核心概念

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
概念说明调节效果
预测时域 (H)优化器向前看的步数H越大前瞻性越强,但计算量增加
Wpower功率跟踪权重越大跟踪越紧密,但控制信号波动可能增大
WΔcost控制平滑度权重越大控制越平滑,减少执行器磨损
超调限制输出偏差的硬约束(如 ≤5%)越小越安全但可能减缓响应速度
+ +
+

🔬 内部线性化模型:本仿真中,涡轴发动机被线性化为以下离散状态方程:

+
+ Nk+1 = Nk + Δt · Kinertia · (Wf,actWf,req)
+ Wf,act,k+1 = (1 − Δt/τ) · Wf,act,k + (Δt/τ) · uk +
+

+ τ 为燃油执行机构时间常数,Kinertia 为转子惯性增益。Wf,req 和功率 P 由 NN 代理模型实时提供。 +

+
+ +
+

⚖️ PID vs MPC 对比:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
特性PIDMPC
原理基于误差的反馈校正(事后调节)基于模型的前馈预测(提前规划)
约束处理需额外加限幅/抗饱和机制在优化中自然处理约束
计算量极低,微秒级较高,需在线优化
模型依赖无需模型需要准确的系统模型
适用场景稳定对象,简单跟踪多变量、有约束的复杂系统
+
+
+
+ +
+ + ✈️ 涡轴发动机模型 + +
+

涡轴发动机由三个耦合子系统建模:

+ + + + + + + + + + + + + + + + + + + + + +
子系统动态特性关键参数
NN 代理模型映射 (高度, 马赫数, 转速) → (燃油流量, 功率);基于 GPR/数据训练输入3维, 输出2维
燃油执行机构一阶惯性环节: dWf/dt = (u − Wf) / τfuelτ 越大响应越慢
转子动力学dN/dt = Kinertia · (Wf,actual − Wf,required)K 越大加速越快
+ +
+

💡 仿真流程:

+
    +
  1. 给定当前转速 N,NN模型预测所需燃油流量 Wf,req 和当前功率 P
  2. +
  3. 控制器(PID或MPC)根据功率误差计算燃油命令 Wf,cmd
  4. +
  5. 执行机构将命令滤波为实际燃油流量 Wf,act
  6. +
  7. Wf,act − Wf,req 产生净力矩驱动转子加速或减速
  8. +
  9. 转速变化又改变NN模型的输入,形成闭环
  10. +
+
+ +
+

📏 性能评价指标:

+
    +
  • 稳态误差:功率达到稳态后与目标的平均偏差
  • +
  • 超调量 (σ%):输出超过目标值的最大百分比
  • +
  • 上升时间 (tr):功率从10%变化到90%所需时间
  • +
  • 调节时间 (ts):功率进入并持续保持在 ±2% 误差带内的时间
  • +
+
+
+
+ +
+ + 📐 PID 控制器 — Laplace 域传递函数 + +
+

连续域 PID 传递函数:

+
+
+ C(s) = Kp + ( + 1 + + + 1 + Ti s + + + Td s + ) +
+
+

其中 Ti = Kp/Ki 为积分时间常数, + Td = Kd/Kp 为微分时间常数。

+ +

Ziegler-Nichols 整定公式表

+

基于极限灵敏度(临界增益)法——设 Ku 为临界增益,Tu 为临界周期:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
控制器类型KpTiTd
P0.5 Ku
PI0.45 KuTu / 1.2
PID0.6 KuTu / 2Tu / 8
+

+ 等价转换:Ki = Kp/Ti,Kd = Kp·Td。 + Z-N 公式通常产生约 25% 超调,实际应用中需进一步微调。 +

+
+
+ +
+ + 🧮 MPC 矩阵化优化问题 + +
+

MPC 将有限时域优化问题转化为二次规划 (QP) 或非线性规划问题:

+ +

离散状态空间模型

+
+
+ xk+1 = Axk + Buk
+ yk = Cxk +
+
+

+ 本系统中:x = [N, Wf,act]T(转速和实际燃油流量), + u = Wf,cmd(燃油命令),y = P(功率输出)。 +

+ +

预测矩阵展开

+

利用递推关系,将 H 步预测写成矩阵形式:

+
+
+ Y = Ψx0 + ΘU +
+
+
    +
  • Y = [y1, ..., yH]T:预测输出序列
  • +
  • U = [u0, ..., uH−1]T:控制输入序列
  • +
  • Ψ = [CA, CA², ..., CAH]T:自由响应矩阵
  • +
  • Θ:Toeplitz 控制响应矩阵
  • +
+ +

带约束 QP 问题

+
+
+ minU (YR)T Q (YR) + ΔUT RΔ ΔU

+ s.t.   umin ≤ uk ≤ umax,   + |yk − rk| ≤ εovershoot +
+
+

+ Q 和 RΔ 分别对应界面中的 Wpower 和 WΔcost 权重滑块。 + 本系统使用 SciPy SLSQP 求解器进行约束非线性优化。 +

+
+
+
+""" +MOTOR_CONTROL_KNOWLEDGE = """ +
+

+ 电机控制器设计理论 +

+ +
+ + ⚙️ PMSM 永磁同步电机模型 + +
+

永磁同步电机 (PMSM) 是电动推进系统的核心驱动组件,具有功率密度高、效率高、控制精度好等优点。

+ +

机械动力学方程

+
+
+ J + + + dt + + = −(Tmotor + Tload + Text + Bω) +
+
+

+ 其中 Tmotor 为电磁转矩(电机产生),Tload 为负载转矩, + Text 为外部扰动,Bω 为粘性摩擦力矩。 +

+ +

电磁转矩表达式

+
+
+ Te = 1.5 · np · ψf · iq +
+
+

+ 对表贴式PMSM (Ld = Lq),转矩仅与q轴电流 iq 成正比。 +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
符号含义典型值对系统的影响
J转动惯量 (kg·m²)0.5 – 5.0越大响应越慢但越平稳
B粘性摩擦系数 (N·m·s)3×10⁻⁴提供自然阻尼
np极对数3影响电气频率和转矩常数
ψf永磁体磁链 (Wb)0.15决定转矩能力和反电动势
Prate额定功率 (kW)300确定最大可用转矩
+ +
+

💡 电压约束与弱磁: + 当转速升高时,反电动势 e = np·ψf·ω 增大。 + 当反电动势接近母线电压 Vdc 时,电机进入弱磁区, + 需注入负的 id 电流来削弱磁链,此时可用转矩下降。 +

+
+
+
+ +
+ + 🎯 转速控制环路 + +
+

典型电机控制采用双环级联结构:外环为转速环,内环为电流环。

+
+ + ωref → [转速控制器] → Tcmd → [电流环 + SVPWM] → PMSM → ωactual + +
+ +
+

PID 模式:

+
    +
  • 根据转速误差 e = ωref − ω 计算转矩命令 T = Kp·e + Ki·∫e + Kd·ė
  • +
  • 转矩命令转换为功率请求 P = T·ω 送入逆变器
  • +
  • 简单可靠,但无法提前预知负载变化
  • +
+
+
+

MPC 模式:

+
    +
  • 利用电机离散动力学模型预测 H 步后的转速轨迹
  • +
  • 优化 H 步的转矩序列,使转速跟踪误差和控制增量同时最小化
  • +
  • 支持超调硬约束:转速偏差不得超过设定百分比
  • +
+
+ minT0..H Σ [ Wωk − ωref)² + WΔ(Tk − Tk−1)² ] +
+

+ 离散状态方程:ωk+1 = (1−dt·B/J)·ωk − (dt/J)·Tcmd − (dt/J)·(Text+Tload) +

+
+
+
+ +
+ + 🔍 负载扰动测试设计 + +
+

本仿真在运行至 60% 时刻时自动施加 50% 负载阶跃扰动, + 用于检验控制器的抗扰性能。

+ +

📊 抗扰性能指标

+ + + + + + + + + + + + + + + + + + + + + +
指标定义理想范围
转速跌落 (Speed Dip)扰动后转速最大瞬时下降量< 5% 额定转速
恢复时间转速回到 ±2% 设定值的时间< 3 秒
稳态误差扰动后新稳态与目标的偏差< 1% 或指定精度
+ +
+

⚙️ 转动惯量 J 的影响:

+
    +
  • J 较大:响应较慢但转速跌落小,系统惯性大不易被扰动"推走"
  • +
  • J 较小:响应快但对扰动更敏感,转速波动大
  • +
  • 实际工程中,可通过飞轮或增大转子质量来调整等效惯量
  • +
+
+ +
+

💡 MPC 调参建议:

+
    +
  • 如果扰动后转速恢复慢→增大 Wspeed 或减小 WΔcost
  • +
  • 如果转速振荡→增大 WΔcost 或减小 Wspeed
  • +
  • 如果超调过大→降低超调限制百分比
  • +
  • 负载很大时,确认电机额定功率是否足以提供所需转矩
  • +
+
+
+
+ +
+ + 📐 PMSM dq 坐标系电压方程 + +
+

通过 Park 变换将三相 ABC 坐标系转换为旋转 dq 坐标系,PMSM 的电压方程简化为:

+
+
+ ud = Rs id + Ld + + did + dt + + − ωe Lq iq
+ uq = Rs iq + Lq + + diq + dt + + + ωe (Ld id + ψf) +
+
+
    +
  • ωe = np · ωm:电气角速度
  • +
  • −ωeLqiq:d 轴耦合项(q 轴电流产生的交叉耦合)
  • +
  • eψf:永磁体反电动势(ωψf = Eback-EMF
  • +
  • 表贴式 PMSM 中 Ld = Lq,无磁阻转矩分量
  • +
+ +

磁场定向控制 (FOC) 原理

+
+

FOC 核心思想:通过控制 id = 0(表贴式),使电磁转矩与 iq 线性正比:

+
+ Te = 1.5 · np · ψf · iq +
+
    +
  • 外环(转速环)→ 给出转矩参考 → 换算为 iq 参考
  • +
  • 内环(电流环)→ PI 控制 id=0, iq=目标值
  • +
  • 逆 Park 变换 → SVPWM 产生三相驱动电压
  • +
+
+ +

坐标变换

+ + + + + + + + + + + + + + + + +
变换作用公式要点
Clarke (ABC→αβ)三相→两相静止坐标iα = ia,iβ = (ia + 2ib)/√3
Park (αβ→dq)静止→旋转坐标id = iαcosθ + iβsinθ
+
+
+
+""" +GPR_KNOWLEDGE = """ +
+

+ 📈 GPR 高斯过程回归 — 理论与公式详解 +

+ +
+ + 📖 什么是高斯过程回归 (Gaussian Process Regression)? + +
+

高斯过程 (Gaussian Process, GP) 是一种定义在函数空间上的随机过程。 + 直觉上,GP 将"先验分布"从有限维的参数空间推广到无穷维的函数空间——它不是对参数建模, + 而是直接对函数本身施加概率分布。

+ +
+

📌 核心定义

+

一个高斯过程是指:对于任意有限输入点集合 {x1, ..., xn}, + 其对应的函数值 [f(x1), ..., f(xn)] 服从联合多元高斯分布。

+
+
+ f(x) ~ GP( m(x),  k(x, x') ) +
+
+
    +
  • 均值函数 m(x) = E[f(x)]:描述函数的先验趋势
  • +
  • 协方差函数(核函数) k(x, x') = Cov[f(x), f(x')]: + 刻画不同输入点处函数值之间的相关性
  • +
+
+ +

与参数化回归的本质区别:

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
方面参数化方法(如线性回归)GPR(非参数贝叶斯)
模型形式f(x) = wTφ(x),参数 w 有限直接在函数空间上建模
复杂度由模型结构(特征维度)固定随数据量自适应增长
不确定性通常只提供点估计天然提供预测方差(置信区间)
小样本容易过拟合或欠拟合先验正则化,小样本表现好
+
+
+ +
+ + 🔬 核函数 (Kernel / Covariance Function) 详解 + +
+

核函数是 GPR 的"灵魂"——它完全决定了 GP 的先验性质(平滑度、周期性、长程相关性等)。 + 核函数 k(x, x') 衡量两个输入点对应函数值的相关程度。

+ +
+ 1️⃣ RBF 核 (径向基函数核 / 平方指数核) +
+
+ kRBF(x, x') = σf2 · exp + ( + − + + xx'‖² + 2ℓ² + + ) +
+
+
    +
  • σf2 (输出尺度):控制函数值的整体变化幅度
  • +
  • (长度尺度):控制平滑度——ℓ 越大,函数变化越缓慢;ℓ 越小,函数变化越剧烈
  • +
  • RBF 核假设函数无限可微,适合建模光滑连续的物理过程
  • +
  • 当 ‖xx'‖ ≫ ℓ 时,k → 0(远距离点不相关)
  • +
+
+ +
+ 2️⃣ Matérn 核(本项目使用 Matérn-5/2) +
+
+ k5/2(r) = σf2 + (1 + √5 · + + r + + + + + 5r² + 3ℓ² + + ) + exp(−√5 · + + r + + + ) +
+
+

其中 r = ‖xx'‖ 为欧氏距离

+
    +
  • Matérn 核是 RBF 的推广,通过参数 ν 控制可微性:ν=1/2 → 不可微; ν=∞ → RBF
  • +
  • ν=5/2:函数两次可微,比 RBF 更灵活,是工程建模的常用选择
  • +
  • 相比 RBF 能更好地捕捉工程数据中的局部不规则性
  • +
+
+ +
+ 3️⃣ ARD (自动相关性判定) 机制 +

对于多维输入 x = [x1, ..., xd],ARD 核为每个输入维度分配独立的长度尺度:

+
+
+ rARD2 = Σj=1d + + (xj − x'j + j² + +
+
+
    +
  • 若 ℓj 很大 → 函数对第 j 维输入不敏感(自动"忽略"该维度)
  • +
  • 若 ℓj 很小 → 函数在第 j 维变化剧烈(高度相关)
  • +
  • ARD 通过数据自动学习每维的重要性,实现隐式特征选择
  • +
  • 本项目:3 个输入 [高度, 马赫数, RPM] → 3 个独立的 ℓj
  • +
+
+
+
+ +
+ + 📐 GPR 后验推断公式(预测均值与方差) + +
+

训练数据:给定 N 个观测 D = {(xi, yi)}i=1N, + 假设 y = f(x) + ε,其中 ε ~ N(0, σn²) 为观测噪声。

+ +

联合先验分布:

+
+
+ [ + + y + f* + + ] + ~ N + ( + + m + m* + + , + + [K + σn²I   K*] + [K*T       K**] + + ) +
+
+ +
+

🎯 后验预测分布(关键公式)

+

对新输入 x*,后验 f* | D, x* 仍为高斯分布:

+
+
+ 预测均值:  + μ* = m(x*) + k*T (K + σn²I)−1 (ym) +
+
+ 预测方差:  + σ*² = k**k*T (K + σn²I)−1 k* +
+
+

其中:

+
    +
  • K ∈ ℝN×N:训练数据之间的核矩阵,Kij = k(xi, xj)
  • +
  • k* ∈ ℝN:测试点与所有训练点之间的核向量
  • +
  • k**:测试点自身的先验方差 k(x*, x*)
  • +
  • (K + σn²I)−1:核心计算瓶颈,复杂度 O(N³)
  • +
+
+ +
+

📊 方差的物理含义:

+
    +
  • σ*² (接近训练数据区域)→ 预测可信度高,GPR "有信心"
  • +
  • σ*² (远离训练数据区域)→ 预测不确定性高,是外推区域
  • +
  • 方差热力图可以直观展示模型"知道什么"和"不知道什么"
  • +
  • 这是 GPR 优于 NN 的核心优势——自动量化认知不确定性
  • +
+
+
+
+ +
+ + ⚙️ 超参数优化 — 最大化边际似然 + +
+

GPR 的超参数 θ = {σf, ℓ1, ..., ℓd, σn} 通过 + 最大化对数边际似然 (Log Marginal Likelihood) 自动确定:

+ +
+
+ log p(y|X, θ) = − + + 1 + 2 + + yTKy−1y + − + + 1 + 2 + + log|Ky| + − + + N + 2 + + log(2π) +
+
+

其中 Ky = K + σn²I

+ +
+

📐 三项分解的物理意义:

+ + + + + + + + + + + + + + + + + + + + + +
数学形式含义
数据拟合项−½ yTKy−1y模型对数据的拟合程度(越大越好拟合)
复杂度惩罚项−½ log|Ky|Occam 剃刀:自动惩罚过于复杂的模型
归一化常数−(N/2) log(2π)与超参数无关,优化时可忽略
+

+ 优化过程在"拟合数据"和"模型简单性"之间自动取得平衡,天然防止过拟合。 + 本项目使用 BoTorch/GPyTorch 框架,通过 L-BFGS 优化器求解。 +

+
+
+
+ +
+ + 🔄 数据预处理流水线 + +
+

发动机数据(燃油流量、功率)的数值范围跨越多个数量级,直接建模会导致核函数 + 无法有效捕捉小值区域的变化。采用两步预处理:

+ +
+
    +
  1. + Log1p 变换: ylog = log(1 + y)
    + → 压缩大值,拉伸小值,使数据分布更均匀。log1p(0)=0,避免 log(0) 的数值问题。 +
  2. +
  3. + Z-Score 标准化: ŷ = (ylog − μ) / σ
    + → 标准化为均值 0、标准差 1,使核函数的超参数在同一尺度上可比。 +
  4. +
  5. + 反变换(预测时):
    + ylog = ŷ · σ + μ   →   y = expm1(ylog) = eylog − 1
    + → 从标准化空间恢复到物理空间 +
  6. +
+
+ +
+

💡 相同的预处理同时应用于 GPR 和 NN:

+
    +
  • GPR 在 Z-Score 空间中训练和预测
  • +
  • NN(蒸馏学生模型)使用 GPR 相同的 scaler 参数
  • +
  • 这确保了两个模型的输入输出在相同数值空间中进行比较
  • +
+
+
+
+ +
+ + 📊 模型评价指标与计算复杂度 + +
+

评价指标公式

+
+

MAPE (Mean Absolute Percentage Error):

+
+ MAPE = + + 1 + N + + Σi=1N + + |yi − ŷi| + yi + + × 100% +
+

注意:MAPE 计算排除 y ≈ 0 的数据点(避免除零导致的虚高误差)

+
+
+

R² (决定系数):

+
+ R² = 1 − + + Σ(yi − ŷi + Σ(yi − ȳ)² + +
+

R² = 1 → 完美预测; R² = 0 → 与均值预测一样差; R² < 0 → 更差

+
+ +

计算复杂度分析

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
操作时间复杂度空间复杂度瓶颈
训练(Cholesky 分解)O(N³)O(N²)核矩阵 K 的 Cholesky 分解
单点预测O(N)O(N)需与所有训练点计算核值
批量预测 (M 点)O(MN + N²)O(MN)大 M 时成为瓶颈
超参数优化O(N³) × 迭代次数O(N²)每次迭代都需 Cholesky
+

+ ⚠️ 正是 O(N³) 的训练复杂度和 O(N) 的单点预测成本,使得 GPR 不适合 MPC 实时控制 + (每个控制步需评估模型数十次),因此需要将 GPR 的知识蒸馏到 O(1) 推理的 NN 中。 +

+
+
+ +
+ + ✈️ GPR 在发动机代理建模中的应用 + +
+

在本项目中,GPR 被用作涡轴发动机的高精度代理模型 (Surrogate Model), + 替代复杂的热力学仿真代码。

+ +
+

建模映射关系:

+
+ [高度(m), 马赫数, RPM]  →  GPR  →  [燃油流量(kg/h), 功率(kW)] +
+ + + + + + + + + + + + + + + + + + + + + +
变量含义典型范围
Altitude_m飞行高度0 ~ 10000 m
Mach飞行马赫数0 ~ 0.8
RPM燃气涡轮转速1000 ~ 30000+
+
+ +
+

💡 为什么选择 GPR 作为代理模型?

+
    +
  • 发动机热力学仿真每次调用耗时秒级,而 GPR 训练后毫秒级预测
  • +
  • 发动机测试数据成本高昂,GPR 在小样本下表现优异
  • +
  • GPR 的方差输出可识别数据稀疏区域,指导后续试验设计
  • +
  • 多输出 GPR (Multi-output GP) 可同时建模燃油流量和功率
  • +
+
+
+
+
+""" + + +# ============================================================ +# NN 神经网络知识蒸馏知识卡片 — 中文 +# ============================================================ +NN_KNOWLEDGE = """ +
+

+ 🧠 NN 神经网络 — 知识蒸馏与 MLP 详解 +

+ +
+ + 📖 知识蒸馏 (Knowledge Distillation) 原理 + +
+

知识蒸馏是一种模型压缩技术, + 将大型"教师"模型(如 GPR)的知识迁移到轻量级"学生"模型(如 MLP 神经网络)中, + 在几乎不损失精度的前提下大幅提升推理速度。

+ +
+
+ [GPR 教师模型] +  → 密集网格预测 →  + [NN 学生模型] +
+
+ +

蒸馏流程

+
+
    +
  1. 训练 GPR 教师:用少量真实发动机数据训练 GPR 模型
  2. +
  3. 生成伪数据:在输入空间的密集网格上用 GPR 预测,生成大量"虚拟标签"
  4. +
  5. 训练 NN 学生:用 GPR 的预测结果作为训练标签,训练 MLP 网络
  6. +
  7. 部署学生网络:在 MPC 控制器中使用轻量 NN 替代 GPR
  8. +
+
+ +

教师 vs 学生对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
指标GPR (教师)NN (学生)
推理速度~10 ms(核矩阵运算)~0.1 ms(矩阵乘法)
内存占用O(N²) — 存储全部训练数据O(W) — 约 4.5K 参数
不确定性预测均值 + 后验方差仅点估计
MPC 适用性推理太慢,不适合实时✅ 满足实时控制需求
批量预测大批量很慢天然并行,GPU 加速
+
+
+ +
+ + 🏗️ MLP 多层感知机架构详解 + +
+

学生网络采用 3 层全连接 MLP (Multi-Layer Perceptron) 架构:

+
+
+ 输入 (3) + → Linear(64) + Tanh + → Linear(64) + Tanh + → Linear(2) + → 输出 (2) +
+
+ +

前向传播数学公式

+

每一层的计算可以表示为仿射变换 + 非线性激活:

+
+
+ 第 l 层:  + z(l) = W(l) a(l−1) + b(l) +
+
+ 激活后:  + a(l) = σ(z(l)) +
+
+
    +
  • W(l) ∈ ℝdl×dl−1:第 l 层的权重矩阵
  • +
  • b(l) ∈ ℝdl:第 l 层的偏置向量
  • +
  • σ(·):激活函数(本项目使用 Tanh)
  • +
  • a(0) = x(输入向量)
  • +
+ +

本网络的完整前向计算

+
+
+ h1 = tanh(W1 · + b1)     // 隐藏层 1: ℝ³ → ℝ⁶⁴
+ h2 = tanh(W2 · h1 + b2)     // 隐藏层 2: ℝ⁶⁴ → ℝ⁶⁴
+ ŷ = W3 · h2 + b3             // 输出层: ℝ⁶⁴ → ℝ² (无激活) +
+
+ +

参数量计算

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
形状权重数偏置数合计
Linear-13 → 6419264256
Linear-264 → 644096644160
Linear-364 → 21282130
总参数量4,546
+
+
+ +
+ + 📐 激活函数详解 + +
+

激活函数为神经网络引入非线性——没有激活函数,无论多少层的网络都等价于单层线性变换。

+ +
+ Tanh (双曲正切) — 本项目所用 +
+
+ tanh(x) = + + ex − e−x + ex + e−x + +
+
+
    +
  • 输出范围:(−1, +1),以零为中心
  • +
  • 导数:tanh'(x) = 1 − tanh²(x),最大值为 1(x=0 处)
  • +
  • 优点:输出零中心,有助于加速收敛
  • +
  • 缺点:|x| 较大时梯度接近 0(梯度饱和),deep网络可能出现梯度消失
  • +
+
+ +

常用激活函数对比

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
函数公式范围特点
Sigmoid1 / (1 + e−x)(0, 1)输出非零中心,双端饱和
Tanh(ex−e−x)/(ex+e−x)(−1, 1)零中心,双端饱和
ReLUmax(0, x)[0, ∞)计算高效,但有"死神经元"问题
GELUx · Φ(x)连续Transformer 常用,性能好
+

对于本项目的回归任务(光滑连续的发动机特性映射),Tanh 是良好的选择, + 因为数据经过 Z-Score 标准化后值域在 [−3, 3] 左右,刚好在 Tanh 的线性区间内。

+
+
+ +
+ + 📉 损失函数与反向传播 + +
+

损失函数 (Loss Function)

+

本项目在标准化 log 空间中使用 MSE (Mean Squared Error) 损失:

+
+
+ L = + + 1 + N + + Σi=1N ‖ŷi − yi‖² +
+
+

+ 其中 ŷ 和 y 均为标准化 log 空间中的值(先 log1p 变换,再 Z-Score)。 + 在标准化空间中训练使得燃油流量和功率两个输出的损失贡献相当。 +

+ +

反向传播算法 (Backpropagation)

+

反向传播基于链式法则 (Chain Rule),从输出层到输入层逐层计算梯度:

+
+
+ 输出层梯度:  + + ∂L + ∂W3 + + = + + ∂L + ∂ŷ + + · h2T +
+ 隐藏层梯度:  + + ∂L + ∂Wl + + = δ(l) · a(l−1)T +   其中  δ(l) = (Wl+1T δ(l+1)) ⊙ σ'(z(l)) +
+
+
    +
  • 表示逐元素乘法 (Hadamard product)
  • +
  • σ' 为激活函数的导数:tanh'(x) = 1 − tanh²(x)
  • +
  • 反向传播的梯度从后向前"流动",自动计算所有参数的偏导数
  • +
  • PyTorch 的 autograd 引擎自动构建计算图并执行反向传播
  • +
+
+
+ +
+ + 🔧 优化器与学习率调度 + +
+

Adam 优化器

+

Adam (Adaptive Moment Estimation) 结合了动量 (Momentum) 和 RMSProp 的优点:

+
+
+ mt = β1 mt−1 + (1−β1) gt +   // 一阶矩(梯度均值)
+ vt = β2 vt−1 + (1−β2) gt² +   // 二阶矩(梯度方差)
+ t = mt / (1−β1t) +                  + // 偏差修正
+ t = vt / (1−β2t)
+ θt+1 = θt − η · t / (√t + ε) +
+
+
    +
  • β1=0.9, β2=0.999:动量和二阶矩的衰减率(默认值)
  • +
  • ε=10⁻⁸:防止除零的小常数
  • +
  • η:初始学习率(本项目约 1e-3 ~ 5e-3)
  • +
  • Adam 为每个参数维护独立的自适应学习率
  • +
+ +

余弦退火学习率调度 (Cosine Annealing)

+
+
+ ηt = ηmin + + + ηmax − ηmin + 2 + + ( + 1 + cos + ( + + πt + T + + ) + ) +
+
+
    +
  • 学习率从 ηmax 平滑下降到 ηmin,呈余弦形状
  • +
  • 训练初期学习率大 → 快速探索参数空间
  • +
  • 训练后期学习率小 → 精细化调整,不会跳出最优解
  • +
  • 比阶梯式衰减更平滑,收敛更稳定
  • +
+
+
+ +
+ + 🔄 数据处理流水线与超参数建议 + +
+

完整数据流水线

+
+
    +
  1. 原始输入:[高度(m), 马赫数, 转速(RPM)]
  2. +
  3. 输入 Z-Score:x̂ = (x − μx) / σx
  4. +
  5. NN 前向传播:ŷnorm = MLP(x̂)
  6. +
  7. 反 Z-Score:ŷlog = ŷnorm · σy + μy
  8. +
  9. 反 Log1p:ŷ = expm1(ŷlog) = eŷlog − 1
  10. +
  11. 物理输出:[燃油流量(kg/h), 功率(kW)]
  12. +
+
+ +

超参数调优建议

+ + + + + + + + + + + + + + + + + + + + + + + + + +
超参数推荐范围增大效果减小效果
隐藏层宽度32 ~ 128拟合能力增强,速度稍降推理更快,但可能欠拟合
训练轮数2000 ~ 5000精度更高,但耗时增加训练快,但可能未收敛
学习率1e-3 ~ 5e-3收敛更快,但容易振荡收敛更稳,但可能太慢
+ +
+

🎯 观察训练 Loss 曲线的注意事项:

+
    +
  • Loss 持续下降:正常训练中,一切良好
  • +
  • Loss 振荡不降:学习率可能过大,尝试降低
  • +
  • Loss 很快收敛到平台:模型容量不足(增大隐藏层宽度)或学习率太小
  • +
  • Parity Plot 偏离对角线:模型预测有系统偏差,可能需要更多训练轮数
  • +
  • 典型目标:MAPE < 1%,即 NN 与 GPR 的预测差异小于 1%
  • +
+
+
+
+
+""" + + +# ============================================================ +# 能量管理策略知识卡片 — 中文 +# ============================================================ +EMS_KNOWLEDGE = """ +
+

+ 🔋 串联混动能量管理策略 (EMS) +

+ +
+ + 📊 SOC 迟滞控制 (Hysteresis Control) + +
+

EMS 采用迟滞(滞回)控制策略,在"充电模式"与"功率跟随模式" + 之间引入死区,避免因 SOC 在阈值附近波动而导致频繁切换:

+ +
+
+ SOC < SOClow充电模式(发动机以固定高功率运行)
+ SOC > SOChigh功率跟随模式(发动机跟踪需求功率)
+ SOClow ≤ SOC ≤ SOChigh保持当前模式(迟滞死区) +
+
+ +

运行模式详解

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模式发动机功率触发条件设计目的
紧急充电最大功率 Peng,maxSOC < 10%防止电池深放电损坏
充电模式恒定充电功率 PchargeSOC < SOClow(进入)高效率恒定工况点运行
功率跟随Pdemand + Reserve + KSOC·ΔSOCSOC > SOChigh(退出充电)实时匹配负载需求
过充保护最低功率 Peng,minSOC > 95%防止电池过充损坏
+ +
+

💡 迟滞控制原理:

+
    +
  • 上阈值 SOChigh(如 0.7):SOC 超过此值时从充电切换到功率跟随
  • +
  • 下阈值 SOClow(如 0.3):SOC 降到此值时从功率跟随切换到充电
  • +
  • 死区宽度 = SOChigh − SOClow,越宽则切换频率越低,但 SOC 波动范围越大
  • +
  • 死区内保持上一步的运行模式不变 → 有效抑制"抖动"(chattering)
  • +
+
+
+
+ +
+ + 🔌 串联混合动力架构 + +
+
+ + [涡轴发动机] → 发电机 → 直流母线 ← → [锂电池组]
+ 直流母线 → 逆变器 → [PMSM 驱动电机] → 螺旋桨 +
+
+

发动机与推进轴机械解耦:所有动力通过电气母线传输, + 允许发动机始终运行在最优效率点,不受负载功率变化的直接影响。

+ +

架构特点

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
特性串联构型并联构型(对比)
发动机-负载耦合完全解耦机械直连
发动机工况可固定在最优效率点随负载波动
EMS 复杂度较低(功率分配)较高(扭矩耦合)
能量传递效率经两次电能转换(略低)机械直驱(略高)
+
+
+ +
+ + ⚙️ 关键参数与调节指南 + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
参数作用调大效果调小效果
Pcharge充电模式发动机功率SOC 恢复更快充电更温和,效率稍高
KSOC功率跟随模式 SOC 反馈增益SOC 偏差纠正更强发动机功率更平稳
功率裕度 %发动机额外功率余量抗瞬态能力增强油耗更低
电池容量能量缓冲区大小SOC 波动更小系统更轻、响应更快
+ +
+

🎯 仿真观察要点:

+
    +
  • SOC 是否维持在安全范围(10% ~ 95%)内
  • +
  • 充电-跟随模式切换是否过于频繁(理想为低频切换)
  • +
  • 发动机功率曲线是否平稳(频繁剧烈波动 → 油耗升高、寿命下降)
  • +
  • 最终 SOC 与初始 SOC 之差(SOC 平衡性)
  • +
  • 电池充放电电流是否超过安全限值
  • +
+
+ +
+

💡 调参建议:

+
    +
  • 先用默认参数跑一次,观察 SOC 整体走势
  • +
  • 若 SOC 持续下降 → 增大 Pcharge 或 KSOC
  • +
  • 若模式切换过于频繁 → 加大 SOChigh − SOClow 死区宽度
  • +
  • 若发动机功率波动大 → 降低 KSOC,增大功率裕度
  • +
+
+
+
+ +
+ + 🔋 电池 SOC 动态方程与功率平衡 + +
+

SOC 状态方程(库仑计数法)

+
+
+ + dSOC + dt + + = − + + Pbat + Qbat + +
+
+

+ Pbat 为电池充放电功率(放电为正),Qbat 为电池总容量 (kWh)。 + 离散化:SOCk+1 = SOCk − (Pbat,k · Δt) / Qbat +

+ +

系统功率平衡方程

+
+
+ Pdemand(t) = Pengine(t) + Pbattery(t) +
+
+
    +
  • Pdemand > Pengine:电池补充差额(放电),SOC 下降
  • +
  • Pdemand < Pengine:多余功率给电池充电,SOC 上升
  • +
  • EMS 的核心任务就是合理分配 Pengine,使 SOC 维持在安全范围
  • +
+ +

功率跟随模式公式

+
+
+ Peng,cmd = Pdemand × (1 + Reserve%) + KSOC × (SOCtarget − SOC)
+ Peng,cmd = clamp(Peng,cmd, Peng,min, Peng,max) +
+
+

+ 第二项为 SOC 反馈补偿:当 SOC 低于目标时增加发动机功率以充电; + 当 SOC 高于目标时减少发动机功率以节油。 +

+
+
+ +
+ + 📚 进阶:ECMS 等效消耗最小化策略 + +
+

ECMS (Equivalent Consumption Minimization Strategy) 是一种更先进的实时优化 EMS。 + 它将电池充放电等效为燃油消耗,从而将双能源分配问题转化为单目标瞬时优化:

+ +
+
+ minPeng   + J = ṁfuel(Peng) + s(t) · + + Pbat + ηbat · QLHV + +
+
+
    +
  • s(t):等效因子,将电能消耗折算为燃油消耗
  • +
  • QLHV:燃料低热值 (MJ/kg)
  • +
  • ηbat:电池充放电效率
  • +
  • 当 s(t) 自适应调整以维持 SOC 时,ECMS 近似全局最优
  • +
+ +
+

💡 本平台使用的是基于规则的滞环策略(简单可靠), + ECMS 和 DP (动态规划) 等高级方法可作为后续扩展方向。

+
+
+
+
+""" diff --git a/case_demo_functions.py b/case_demo_functions.py index 7301670..9a09917 100644 --- a/case_demo_functions.py +++ b/case_demo_functions.py @@ -1,145 +1,1049 @@ import os import sys -# ===== 新增:OpenMP冲突兼容设置,避免PyTorch初始化报错 ===== +# ===== OpenMP冲突兼容设置,避免PyTorch初始化报错 ===== os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE") import numpy as np import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt +# English-only plot style (no Chinese fonts needed) +plt.rcParams['font.family'] = 'serif' +plt.rcParams['font.serif'] = ['DejaVu Serif', 'Times New Roman'] +plt.rcParams['axes.unicode_minus'] = True + MODEL_SRC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Model", "src") if MODEL_SRC_PATH not in sys.path: - # ===== 新增:将混动模型源码路径加入导入搜索路径 ===== sys.path.insert(0, MODEL_SRC_PATH) +MODEL_DATA_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Model", "data") + +# ============================================================ +# 阶段零:模型蒸馏演示 (GPR/CSV → NN) +# ============================================================ +def run_distillation_demo(epochs, learning_rate, hidden_size, progress=None): + """ + 运行蒸馏并返回训练曲线 + 对比散点图。 + """ + try: + from distill_gpr_to_nn import distill_from_csv + + csv_path = os.path.join(MODEL_DATA_PATH, "Cleaned_Engine_Data_Full.csv") + nn_path = os.path.join(MODEL_DATA_PATH, "engine_nn_proxy.pth") + + epochs = int(np.clip(epochs, 500, 8000)) + learning_rate = float(np.clip(learning_rate, 1e-4, 1e-2)) + hidden_size = int(np.clip(hidden_size, 16, 256)) + + if progress is not None: + progress(0.0, desc="加载数据...") + + def _progress_cb(epoch, total, loss): + if progress is not None: + progress(epoch / total, desc=f"训练中 Epoch {epoch}/{total}, Loss={loss:.6f}") + + result = distill_from_csv( + csv_path, nn_path, + epochs=epochs, lr=learning_rate, hidden_size=hidden_size, + verbose=True, progress_callback=_progress_cb + ) + + loss_hist = result['loss_history'] + Y_true = result['Y_train'] + Y_pred = result['Y_nn'] + + # ---- Plots ---- + fig, axes = plt.subplots(1, 3, figsize=(16, 5)) + fig.suptitle('GPR / Data → NN Knowledge Distillation Results', + fontweight='bold', fontsize=13) + + # Loss curve + axes[0].semilogy(loss_hist, 'b-', lw=1.2) + axes[0].set_xlabel('Epoch') + axes[0].set_ylabel('MSE Loss (log scale)') + axes[0].set_title('Training Loss Curve') + axes[0].grid(True, linestyle=':', alpha=0.7) + + # Fuel flow parity + axes[1].scatter(Y_true[:, 0], Y_pred[:, 0], s=8, alpha=0.5, c='tab:orange') + lim = [0, max(Y_true[:, 0].max(), Y_pred[:, 0].max()) * 1.05] + axes[1].plot(lim, lim, 'k--', lw=1, alpha=0.7) + axes[1].set_xlabel('True Fuel Flow (kg/h)') + axes[1].set_ylabel('NN Predicted Fuel Flow (kg/h)') + axes[1].set_title(f'Fuel Flow Parity (MAPE={result["rel_error_fuel"]:.2f}%)') + axes[1].set_xlim(lim); axes[1].set_ylim(lim) + axes[1].set_aspect('equal') + axes[1].grid(True, linestyle=':', alpha=0.7) + + # Power parity + axes[2].scatter(Y_true[:, 1], Y_pred[:, 1], s=8, alpha=0.5, c='tab:blue') + lim = [0, max(Y_true[:, 1].max(), Y_pred[:, 1].max()) * 1.05] + axes[2].plot(lim, lim, 'k--', lw=1, alpha=0.7) + axes[2].set_xlabel('True Power (kW)') + axes[2].set_ylabel('NN Predicted Power (kW)') + axes[2].set_title(f'Power Parity (MAPE={result["rel_error_power"]:.2f}%)') + axes[2].set_xlim(lim); axes[2].set_ylim(lim) + axes[2].set_aspect('equal') + axes[2].grid(True, linestyle=':', alpha=0.7) + + fig.tight_layout(rect=[0, 0, 1, 0.94]) + + n_params = sum(p.numel() for p in result['nn_model'].parameters()) + summary = ( + f"### Distillation Results\n" + f"- **NN Architecture**: MLP 3→{hidden_size}→{hidden_size}→2 (Tanh)\n" + f"- **Parameters**: {n_params:,}\n" + f"- **Training Samples**: {len(Y_true)}\n" + f"- **Epochs**: {epochs}, LR: {learning_rate:.1e}\n" + f"- **Final Loss**: {loss_hist[-1]:.6f}\n" + f"- **Fuel Flow MAPE**: {result['rel_error_fuel']:.2f}%\n" + f"- **Power MAPE**: {result['rel_error_power']:.2f}%\n" + f"- **Model saved** to `Model/data/engine_nn_proxy.pth`" + ) + + return fig, summary + + except Exception as e: + import traceback + return None, f"Distillation failed: {e}\n```\n{traceback.format_exc()}\n```" + + +# ============================================================ +# GPR 模型训练/加载 与 可视化 +# ============================================================ +def run_gpr_training(mode="load", progress=None): + """ + GPR 模型训练或加载已有模型,并生成可视化图表。 + + Parameters + ---------- + mode : str + "train" — 从头训练(需 botorch/gpytorch/sklearn) + "load" — 加载已有的 .pth 权重文件 + progress : gr.Progress or None + """ + import csv as csv_mod + try: + csv_path = os.path.join(MODEL_DATA_PATH, "Cleaned_Engine_Data_Full.csv") + gpr_pth = os.path.join(MODEL_DATA_PATH, "engine_gpr_model.pth") + + # ---------- 读取 CSV 原始数据(不依赖 pandas)---------- + if progress is not None: + progress(0.05, desc="读取 CSV 数据...") + with open(csv_path, 'r', encoding='utf-8') as f: + reader = csv_mod.reader(f) + header = next(reader) + rows = [r for r in reader] + col_idx = {name: i for i, name in enumerate(header)} + data = np.array([[float(x) for x in r] for r in rows], dtype=np.float64) + X_cols = ['Altitude_m', 'Mach', 'RPM'] + Y_cols = ['WF_kg_h', 'Power_kW'] + X_raw = data[:, [col_idx[c] for c in X_cols]] + Y_raw = data[:, [col_idx[c] for c in Y_cols]] + n_total = len(data) + + # ---------- 尝试导入 GPR 依赖 ---------- + gpr_available = False + gpr_model = None + try: + from engine_gpr_class import EngineGPRModel + gpr_available = True + except ImportError: + gpr_available = False + + if mode == "train": + if not gpr_available: + return None, ("### ⚠️ GPR 训练失败\n\n" + "缺少依赖包:`botorch`, `gpytorch`, `sklearn`。\n\n" + "请执行 `pip install botorch gpytorch scikit-learn` 后重试," + "或选择 **加载已有模型** 模式。") + if progress is not None: + progress(0.10, desc="初始化 GPR 模型 ...") + gpr_model = EngineGPRModel(csv_path=csv_path) + if progress is not None: + progress(0.15, desc="训练 GPR(超参数优化中)...") + gpr_model.train(save_path=gpr_pth) + if progress is not None: + progress(0.80, desc="GPR 训练完成,生成可视化 ...") + + elif mode == "load": + if not gpr_available: + # --- 无 botorch:仅展示原始数据统计 --- + if progress is not None: + progress(0.30, desc="绘制数据统计图 ...") + fig = _plot_data_overview(X_raw, Y_raw, X_cols, Y_cols) + summary = ( + f"### 📊 数据概览(无 GPR 依赖)\n" + f"- **数据集**: Cleaned_Engine_Data_Full.csv\n" + f"- **样本数**: {n_total}\n" + f"- **输入特征**: {', '.join(X_cols)}\n" + f"- **输出目标**: {', '.join(Y_cols)}\n\n" + f"> ⚠️ 未安装 `botorch`/`gpytorch`,无法加载 GPR 模型。\n" + f"> 请执行 `pip install botorch gpytorch scikit-learn` 后重试。" + ) + return fig, summary + + if not os.path.exists(gpr_pth): + return None, ("### ⚠️ 未找到已训练的 GPR 权重文件\n\n" + f"路径:`{gpr_pth}`\n\n" + "请先选择 **从头训练** 模式。") + if progress is not None: + progress(0.10, desc="加载 GPR 模型 ...") + gpr_model = EngineGPRModel(csv_path=csv_path) + ok = gpr_model.load_model(pth_path=gpr_pth) + if not ok: + return None, "### ⚠️ GPR 模型加载失败,请检查权重文件完整性。" + if progress is not None: + progress(0.40, desc="加载完成,生成可视化 ...") + + # ---------- GPR 模型已就绪,生成可视化 ---------- + if progress is not None: + progress(0.50, desc="GPR 网格预测 ...") + + # 预测网格 + H_range = np.linspace(X_raw[:, 0].min(), X_raw[:, 0].max(), 40) + Ma_range = np.linspace(X_raw[:, 1].min(), X_raw[:, 1].max(), 5) + RPM_range = np.linspace(max(X_raw[:, 2].min(), 1000), X_raw[:, 2].max(), 40) + H, Ma, RPM = np.meshgrid(H_range, Ma_range, RPM_range, indexing='ij') + X_grid = np.column_stack([H.ravel(), Ma.ravel(), RPM.ravel()]) + + pred_mean, pred_var = gpr_model.predict(X_grid) + + if progress is not None: + progress(0.75, desc="绘图中 ...") + + # 在训练数据点上的预测精度(过滤极小值,与 NN 训练一致) + valid_mask = (Y_raw[:, 0] > 0.5) & (Y_raw[:, 1] > 0.5) & (X_raw[:, 2] > 500) + X_eval = X_raw[valid_mask] + Y_eval = Y_raw[valid_mask] + train_pred, _ = gpr_model.predict(X_eval) + mape_fuel = np.mean(np.abs(train_pred[:, 0] - Y_eval[:, 0]) / np.maximum(Y_eval[:, 0], 1e-6)) * 100 + mape_power = np.mean(np.abs(train_pred[:, 1] - Y_eval[:, 1]) / np.maximum(Y_eval[:, 1], 1e-6)) * 100 + + # ---- Plot ---- + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle('GPR Model Training / Validation Results', fontweight='bold', fontsize=13) + + # Fuel flow parity + axes[0, 0].scatter(Y_eval[:, 0], train_pred[:, 0], s=10, alpha=0.5, c='tab:orange') + lim = [0, max(Y_eval[:, 0].max(), train_pred[:, 0].max()) * 1.05] + axes[0, 0].plot(lim, lim, 'k--', lw=1) + axes[0, 0].set_xlabel('True Fuel Flow (kg/h)') + axes[0, 0].set_ylabel('GPR Predicted Fuel Flow (kg/h)') + axes[0, 0].set_title(f'Fuel Flow Parity (MAPE={mape_fuel:.2f}%)') + axes[0, 0].set_xlim(lim); axes[0, 0].set_ylim(lim) + axes[0, 0].set_aspect('equal'); axes[0, 0].grid(True, ls=':', alpha=0.7) + + # Power parity + axes[0, 1].scatter(Y_eval[:, 1], train_pred[:, 1], s=10, alpha=0.5, c='tab:blue') + lim = [0, max(Y_eval[:, 1].max(), train_pred[:, 1].max()) * 1.05] + axes[0, 1].plot(lim, lim, 'k--', lw=1) + axes[0, 1].set_xlabel('True Power (kW)') + axes[0, 1].set_ylabel('GPR Predicted Power (kW)') + axes[0, 1].set_title(f'Power Parity (MAPE={mape_power:.2f}%)') + axes[0, 1].set_xlim(lim); axes[0, 1].set_ylim(lim) + axes[0, 1].set_aspect('equal'); axes[0, 1].grid(True, ls=':', alpha=0.7) + + # Variance heatmap (Mach=0 slice) + ma0_idx = np.argmin(np.abs(Ma_range - 0.0)) + var_reshaped = pred_var.reshape(len(H_range), len(Ma_range), len(RPM_range), 2) + wf_var_slice = var_reshaped[:, ma0_idx, :, 0] + pow_var_slice = var_reshaped[:, ma0_idx, :, 1] + + RPM_g, H_g = np.meshgrid(RPM_range, H_range) + cf0 = axes[1, 0].contourf(RPM_g, H_g, np.log10(np.maximum(wf_var_slice, 1e-16)), + levels=30, cmap='jet', alpha=0.85) + axes[1, 0].set_xlabel('RPM') + axes[1, 0].set_ylabel('Altitude (m)') + axes[1, 0].set_title('Fuel Flow Variance (log₁₀, Mach=0)') + fig.colorbar(cf0, ax=axes[1, 0], shrink=0.8) + + cf1 = axes[1, 1].contourf(RPM_g, H_g, np.log10(np.maximum(pow_var_slice, 1e-16)), + levels=30, cmap='jet', alpha=0.85) + axes[1, 1].set_xlabel('RPM') + axes[1, 1].set_ylabel('Altitude (m)') + axes[1, 1].set_title('Power Variance (log₁₀, Mach=0)') + fig.colorbar(cf1, ax=axes[1, 1], shrink=0.8) + + fig.tight_layout(rect=[0, 0, 1, 0.94]) + + mode_label = "从头训练" if mode == "train" else "加载已有模型" + import torch as _torch + device_info = "CUDA" if _torch.cuda.is_available() else "CPU" + summary = ( + f"### GPR 模型结果\n" + f"- **模式**: {mode_label}\n" + f"- **计算设备**: {device_info}\n" + f"- **训练样本**: {n_total}(有效评估样本: {int(valid_mask.sum())})\n" + f"- **网格预测点**: {len(X_grid)}\n" + f"- **Fuel Flow MAPE**: {mape_fuel:.2f}%\n" + f"- **Power MAPE**: {mape_power:.2f}%\n" + f"- **模型文件**: `Model/data/engine_gpr_model.pth`" + ) + + if progress is not None: + progress(1.0, desc="完成") + return fig, summary + + except Exception as e: + import traceback + return None, f"GPR 训练/加载失败: {e}\n```\n{traceback.format_exc()}\n```" + + +def _plot_data_overview(X_raw, Y_raw, X_cols, Y_cols): + """当 GPR 依赖不可用时,仅绘制原始数据统计概览。""" + fig, axes = plt.subplots(2, 2, figsize=(14, 10)) + fig.suptitle('Engine Data Overview (GPR dependencies unavailable)', fontweight='bold', fontsize=13) + + # Altitude vs Fuel Flow + axes[0, 0].scatter(X_raw[:, 0], Y_raw[:, 0], s=6, alpha=0.4, c='tab:orange') + axes[0, 0].set_xlabel('Altitude (m)') + axes[0, 0].set_ylabel('Fuel Flow (kg/h)') + axes[0, 0].set_title('Altitude vs Fuel Flow') + axes[0, 0].grid(True, ls=':', alpha=0.7) + + # RPM vs Power + axes[0, 1].scatter(X_raw[:, 2], Y_raw[:, 1], s=6, alpha=0.4, c='tab:blue') + axes[0, 1].set_xlabel('RPM') + axes[0, 1].set_ylabel('Power (kW)') + axes[0, 1].set_title('RPM vs Power') + axes[0, 1].grid(True, ls=':', alpha=0.7) + + # RPM vs Fuel Flow colored by Mach + sc = axes[1, 0].scatter(X_raw[:, 2], Y_raw[:, 0], s=6, alpha=0.4, c=X_raw[:, 1], cmap='viridis') + axes[1, 0].set_xlabel('RPM') + axes[1, 0].set_ylabel('Fuel Flow (kg/h)') + axes[1, 0].set_title('RPM vs Fuel Flow (color=Mach)') + fig.colorbar(sc, ax=axes[1, 0], shrink=0.8, label='Mach') + axes[1, 0].grid(True, ls=':', alpha=0.7) + + # Fuel Flow vs Power + axes[1, 1].scatter(Y_raw[:, 0], Y_raw[:, 1], s=6, alpha=0.4, c='tab:green') + axes[1, 1].set_xlabel('Fuel Flow (kg/h)') + axes[1, 1].set_ylabel('Power (kW)') + axes[1, 1].set_title('Fuel Flow vs Power') + axes[1, 1].grid(True, ls=':', alpha=0.7) + + fig.tight_layout(rect=[0, 0, 1, 0.94]) + return fig + + +# ============================================================ +# 阶段一:发动机控制器设计 (PID / MPC 可选) +# ============================================================ +def run_engine_design(sim_time_s, dt, initial_power_kw, target_power_kw, + controller_type, + kp, ki, kd, tau_fuel, K_inertia, + mpc_horizon, mpc_W_power, mpc_W_dcost, mpc_overshoot_limit, + progress=None): + """发动机控制器阶跃响应仿真""" + try: + import torch + from lightweight_model import EngineNNProxy + + # 参数裁剪 + sim_time_s = float(np.clip(sim_time_s, 5, 120)) + dt = float(np.clip(dt, 0.01, 0.2)) + initial_power_kw = float(np.clip(initial_power_kw, 20, 260)) + target_power_kw = float(np.clip(target_power_kw, 20, 300)) + tau_fuel = float(np.clip(tau_fuel, 0.02, 2.0)) + K_inertia = float(np.clip(K_inertia, 5, 1000)) + + nn_pth = os.path.join(MODEL_DATA_PATH, "engine_nn_proxy.pth") + if not os.path.exists(nn_pth): + return None, "Error: `engine_nn_proxy.pth` not found. Please run the **Distillation** tab first." + + engine_nn = EngineNNProxy() + engine_nn.load_state_dict(torch.load(nn_pth, map_location='cpu')) + engine_nn.eval() + + # 初始稳态转速 — 纯Python二分法(不依赖scipy) + def _bisect(func, a, b, tol=1e-4, maxiter=50): + fa, fb = func(a), func(b) + if fa * fb > 0: + return a if abs(fa) < abs(fb) else b + for _ in range(maxiter): + c = (a + b) / 2.0 + fc = func(c) + if abs(fc) < tol or (b - a) / 2 < tol: + return c + if fa * fc < 0: + b, fb = c, fc + else: + a, fa = c, fc + return (a + b) / 2.0 + + def _solve_rpm(target_p): + def obj(n): + inp = torch.tensor([[0.0, 0.0, n]], dtype=torch.float32) + with torch.no_grad(): + return engine_nn(inp).numpy()[0, 1] - target_p + return _bisect(obj, 1000, 58000) + + N_current = _solve_rpm(initial_power_kw) + with torch.no_grad(): + pred0 = engine_nn(torch.tensor([[0.0, 0.0, N_current]], dtype=torch.float32)).numpy() + Wf_act = pred0[0, 0] + Wf_cmd = Wf_act + + use_mpc = (controller_type == "MPC") + + if use_mpc: + from mpc_controller import TurboShaftMPCController + mpc_horizon = int(np.clip(mpc_horizon, 3, 30)) + mpc_W_power = float(np.clip(mpc_W_power, 1, 1000)) + mpc_W_dcost = float(np.clip(mpc_W_dcost, 0.01, 50)) + mpc_overshoot_limit = float(np.clip(mpc_overshoot_limit, 0.01, 0.30)) + mpc = TurboShaftMPCController( + tau_fuel=tau_fuel, K_inertia=K_inertia, dt=dt, + horizon=mpc_horizon, min_fuel=5.0, max_fuel=400.0, + overshoot_limit=mpc_overshoot_limit + ) + mpc.W_power = mpc_W_power + mpc.W_dcost = mpc_W_dcost + mpc.reset(initial_output=Wf_cmd, initial_N=N_current) + else: + from increPID import IncrementalPIDController + kp = float(np.clip(kp, 0.01, 30)) + ki = float(np.clip(ki, 0.0, 30)) + kd = float(np.clip(kd, 0.0, 10)) + pid = IncrementalPIDController( + kp=kp, ki=ki, kd=kd, dt=dt, + output_min=5.0, output_max=400.0, + input_scale=300.0, output_scale=400.0 + ) + pid.reset(initial_output=Wf_act) + + time_array = np.arange(0, sim_time_s, dt) + t_step = sim_time_s * 0.15 + + logs = {'N': [], 'Wf_act': [], 'Wf_cmd': [], 'Power': [], 'Power_target': []} + + n_steps = len(time_array) + for step_i, t in enumerate(time_array): + if progress is not None and step_i % max(1, n_steps // 20) == 0: + progress(step_i / n_steps, desc=f"发动机仿真 {step_i}/{n_steps} (t={t:.1f}s)") + target_p = initial_power_kw if t < t_step else target_power_kw + + delta_N = 5.0 + batch_inp = torch.tensor([ + [0.0, 0.0, N_current], + [0.0, 0.0, N_current + delta_N] + ], dtype=torch.float32) + with torch.no_grad(): + batch_pred = engine_nn(batch_inp).numpy() + + Wf_req = batch_pred[0, 0] + Power = batch_pred[0, 1] + + if use_mpc: + k_wf = (batch_pred[1, 0] - batch_pred[0, 0]) / delta_N + k_p = (batch_pred[1, 1] - batch_pred[0, 1]) / delta_N + Wf_cmd = mpc.compute( + current_N=N_current, current_Wfact=Wf_act, + target_power=target_p, + precalc_params=(Wf_req, Power, k_wf, k_p) + ) + else: + Wf_cmd = pid.compute(setpoint=target_p, measurement=Power) + + dWf = (Wf_cmd - Wf_act) / tau_fuel + Wf_act += dWf * dt + dN = K_inertia * (Wf_act - Wf_req) + N_current += dN * dt + + logs['N'].append(N_current) + logs['Wf_act'].append(Wf_act) + logs['Wf_cmd'].append(Wf_cmd) + logs['Power'].append(Power) + logs['Power_target'].append(target_p) + + if progress is not None: + progress(1.0, desc="绘图中...") + # ---- Performance metrics ---- + power_arr = np.array(logs['Power']) + step_idx = int(t_step / dt) + post_step = power_arr[step_idx:] + + tail = max(1, len(post_step) // 10) + ss_error = np.mean(np.abs(post_step[-tail:] - target_power_kw)) + + delta = target_power_kw - initial_power_kw + overshoot = 0.0 + if abs(delta) > 1: + if delta > 0: + overshoot = max(0, (np.max(post_step) - target_power_kw) / delta * 100) + else: + overshoot = max(0, (target_power_kw - np.min(post_step)) / abs(delta) * 100) + + rise_time = float('nan') + if abs(delta) > 1: + thresh_10 = initial_power_kw + 0.1 * delta + thresh_90 = initial_power_kw + 0.9 * delta + t10 = t90 = None + for i in range(step_idx, len(power_arr)): + if delta > 0: + if t10 is None and power_arr[i] >= thresh_10: + t10 = time_array[i] - time_array[step_idx] + if t90 is None and power_arr[i] >= thresh_90: + t90 = time_array[i] - time_array[step_idx] + else: + if t10 is None and power_arr[i] <= thresh_10: + t10 = time_array[i] - time_array[step_idx] + if t90 is None and power_arr[i] <= thresh_90: + t90 = time_array[i] - time_array[step_idx] + if t10 is not None and t90 is not None: + rise_time = t90 - t10 + + settling_time = float('nan') + if abs(delta) > 1: + band = abs(delta) * 0.02 + for i in range(len(post_step) - 1, -1, -1): + if abs(post_step[i] - target_power_kw) > band: + settling_time = (i + 1) * dt + break + + # ---- Plots (English) ---- + ctrl_label = "MPC" if use_mpc else "PID" + fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True) + fig.suptitle(f'Engine Controller Step Response ({ctrl_label})', + fontweight='bold', fontsize=13) + + axes[0].plot(time_array, logs['Power_target'], 'k--', lw=1.5, label='Target Power') + axes[0].plot(time_array, logs['Power'], 'r-', lw=1.5, label='Actual Power') + axes[0].set_ylabel('Power (kW)') + axes[0].set_title('Power Tracking') + axes[0].grid(True, linestyle=':'); axes[0].legend() + + axes[1].plot(time_array, logs['N'], 'b-', lw=1.5) + axes[1].set_ylabel('Speed (RPM)') + axes[1].set_title('Engine Rotor Speed') + axes[1].grid(True, linestyle=':') + + axes[2].plot(time_array, logs['Wf_cmd'], 'k--', lw=1.2, label='Fuel Command') + axes[2].plot(time_array, logs['Wf_act'], 'r-', lw=1.2, label='Actual Fuel') + axes[2].set_ylabel('Fuel Flow (kg/h)') + axes[2].set_xlabel('Time (s)') + axes[2].set_title('Fuel Control Signal') + axes[2].grid(True, linestyle=':'); axes[2].legend() + + fig.tight_layout(rect=[0, 0, 1, 0.96]) + + if use_mpc: + param_str = (f"Horizon={mpc_horizon}, W_power={mpc_W_power:.1f}, " + f"W_Δcost={mpc_W_dcost:.2f}, Overshoot≤{mpc_overshoot_limit*100:.0f}%") + else: + param_str = f"Kp={kp:.3f}, Ki={ki:.3f}, Kd={kd:.3f}" + + summary = ( + f"### Engine Controller Results ({ctrl_label})\n" + f"- **Controller**: {ctrl_label} — {param_str}\n" + f"- **Power Step**: {initial_power_kw:.0f} → {target_power_kw:.0f} kW\n" + f"- **Steady-State Error**: {ss_error:.2f} kW\n" + f"- **Overshoot**: {overshoot:.1f}%\n" + f"- **Rise Time (10%-90%)**: {rise_time:.3f} s\n" + f"- **Settling Time (2% band)**: {settling_time:.3f} s\n" + f"- **Fuel Actuator τ**: {tau_fuel:.2f} s | Rotor Inertia K: {K_inertia:.0f}" + ) + return fig, summary + + except Exception as e: + import traceback + return None, f"Engine simulation failed: {e}\n```\n{traceback.format_exc()}\n```" + + +# ============================================================ +# 阶段二:电机控制器设计 (PID / MPC 可选) +# ============================================================ +def run_motor_design(sim_time_s, dt, target_rpm, load_torque, + controller_type, + kp, ki, kd, J, + mpc_W_speed, mpc_W_dcost, mpc_overshoot_limit, + progress=None): + """电机控制器阶跃响应 + 负载扰动仿真""" + try: + from motor_sim import MotorSim + + sim_time_s = float(np.clip(sim_time_s, 5, 120)) + dt = float(np.clip(dt, 0.01, 0.2)) + target_rpm = float(np.clip(target_rpm, 200, 6000)) + load_torque = float(np.clip(load_torque, 5, 500)) + J = float(np.clip(J, 0.1, 10.0)) + + use_mpc = (controller_type == "MPC") + + if use_mpc: + mpc_W_speed = float(np.clip(mpc_W_speed, 1, 500)) + mpc_W_dcost = float(np.clip(mpc_W_dcost, 0.01, 50)) + mpc_overshoot_limit = float(np.clip(mpc_overshoot_limit, 0.01, 0.30)) + motor = MotorSim( + P_rate=300e3, w_rate=575.95, J=J, + mpc_W_speed=mpc_W_speed, mpc_W_dcost=mpc_W_dcost, + mpc_overshoot_limit=mpc_overshoot_limit, + ) + else: + kp = float(np.clip(kp, 0.01, 80)) + ki = float(np.clip(ki, 0.0, 150)) + kd = float(np.clip(kd, 0.0, 10)) + motor = MotorSim( + P_rate=300e3, w_rate=575.95, J=J, + mpc_W_speed=0.0, mpc_W_dcost=0.0, + mpc_overshoot_limit=0.05, + ) + + time_array = np.arange(0, sim_time_s, dt) + t_step = sim_time_s * 0.10 + t_load_step = sim_time_s * 0.60 + v_bus = 520.0 + p_supply = 0.0 + + if not use_mpc: + from increPID import IncrementalPIDController + w_rate = 575.95 + tau_rate = 300e3 / w_rate + pid_motor = IncrementalPIDController( + kp=kp, ki=ki, kd=kd, dt=dt, + output_min=-tau_rate, output_max=tau_rate, + input_scale=w_rate, output_scale=tau_rate + ) + pid_motor.reset(0.0) + + logs = {'rpm': [], 'target_rpm': [], 'torque': [], + 'p_bus_req': [], 'p_shaft': [], 'p_loss': [], 'load': []} + + n_steps = len(time_array) + for step_i, t in enumerate(time_array): + if progress is not None and step_i % max(1, n_steps // 20) == 0: + progress(step_i / n_steps, desc=f"电机仿真 {step_i}/{n_steps} (t={t:.1f}s)") + n_set = 500.0 if t < t_step else target_rpm + if t < t_step: + load_t = 20.0 + elif t < t_load_step: + load_t = load_torque + else: + load_t = load_torque * 1.5 + + if not use_mpc: + w_set_rad = n_set * 2 * np.pi / 60.0 + T_cmd = pid_motor.compute(setpoint=w_set_rad, measurement=motor.w_M) + w_eff = max(abs(motor.w_M), 1.0) + p_cmd_kw = -T_cmd * w_eff / 1000.0 + state = motor.step(dt=dt, n_setpoint=n_set, p_bus_actual_kw=p_cmd_kw, + v_bus=v_bus, t_load=load_t, t_ext=0.0) + else: + state = motor.step(dt=dt, n_setpoint=n_set, p_bus_actual_kw=p_supply, + v_bus=v_bus, t_load=load_t, t_ext=0.0) + + logs['rpm'].append(state['n_rpm']) + logs['target_rpm'].append(n_set) + logs['torque'].append(state['t_motor']) + logs['p_bus_req'].append(state['p_bus_req_kw']) + logs['p_shaft'].append(state['p_shaft_kw']) + logs['p_loss'].append(state['p_loss_kw']) + logs['load'].append(load_t) + p_supply = state['p_bus_req_kw'] + + if progress is not None: + progress(1.0, desc="绘图中...") + # ---- Performance ---- + rpm_arr = np.array(logs['rpm']) + step_idx = int(t_step / dt) + post_step_rpm = rpm_arr[step_idx:] + + tail = max(1, len(post_step_rpm) // 10) + ss_error = np.mean(np.abs(post_step_rpm[-tail:] - target_rpm)) + + delta_rpm = target_rpm - 500.0 + overshoot = 0.0 + if abs(delta_rpm) > 1 and delta_rpm > 0: + overshoot = max(0, (np.max(post_step_rpm) - target_rpm) / delta_rpm * 100) + + load_step_idx = int(t_load_step / dt) + max_dip = 0 + if load_step_idx < len(rpm_arr): + post_load = rpm_arr[load_step_idx:] + max_dip = max(0, target_rpm - np.min(post_load)) if len(post_load) > 0 else 0 + + # ---- Plots (English) ---- + ctrl_label = "MPC" if use_mpc else "PID" + fig, axes = plt.subplots(3, 1, figsize=(12, 9), sharex=True) + fig.suptitle(f'Motor Controller — Step + Load Disturbance ({ctrl_label})', + fontweight='bold', fontsize=13) + + axes[0].plot(time_array, logs['target_rpm'], 'k--', lw=1.5, label='Target Speed') + axes[0].plot(time_array, logs['rpm'], 'b-', lw=1.5, label='Actual Speed') + axes[0].axvline(t_load_step, color='orange', linestyle=':', lw=1, alpha=0.7, label='Load Disturbance') + axes[0].set_ylabel('Speed (RPM)') + axes[0].set_title('Speed Tracking') + axes[0].grid(True, linestyle=':'); axes[0].legend() + + axes[1].plot(time_array, logs['torque'], 'r-', lw=1.2, label='Motor Torque') + axes[1].plot(time_array, logs['load'], 'k--', lw=1, alpha=0.6, label='Load Torque') + axes[1].set_ylabel('Torque (Nm)') + axes[1].set_title('Torque Response') + axes[1].grid(True, linestyle=':'); axes[1].legend() + + axes[2].plot(time_array, logs['p_bus_req'], 'g-', lw=1.2, label='Bus Power Request') + axes[2].plot(time_array, logs['p_shaft'], 'b--', lw=1.2, label='Shaft Power') + axes[2].plot(time_array, logs['p_loss'], 'r:', lw=1.2, label='Loss Power') + axes[2].set_ylabel('Power (kW)') + axes[2].set_xlabel('Time (s)') + axes[2].set_title('Power Distribution') + axes[2].grid(True, linestyle=':'); axes[2].legend() + + fig.tight_layout(rect=[0, 0, 1, 0.96]) + + if use_mpc: + param_str = (f"W_speed={mpc_W_speed:.1f}, W_Δcost={mpc_W_dcost:.2f}, " + f"Overshoot≤{mpc_overshoot_limit*100:.0f}%") + else: + param_str = f"Kp={kp:.3f}, Ki={ki:.3f}, Kd={kd:.3f}" + + summary = ( + f"### Motor Controller Results ({ctrl_label})\n" + f"- **Controller**: {ctrl_label} — {param_str}\n" + f"- **Speed Step**: 500 → {target_rpm:.0f} RPM\n" + f"- **Load Torque**: {load_torque:.0f} Nm → {load_torque*1.5:.0f} Nm\n" + f"- **Steady-State Error**: {ss_error:.1f} RPM\n" + f"- **Overshoot**: {overshoot:.1f}%\n" + f"- **Max Load Dip**: {max_dip:.1f} RPM\n" + f"- **Inertia J**: {J:.2f} kg·m²" + ) + return fig, summary + + except Exception as e: + import traceback + return None, f"Motor simulation failed: {e}\n```\n{traceback.format_exc()}\n```" + + +# ============================================================ +# 工况配置辅助 +# ============================================================ def _profile_points(profile_name): if profile_name == "高机动阶跃": - return [ - (0.0, 1600.0, 70.0), - (8.0, 3200.0, 220.0), - (20.0, 2500.0, 130.0), - (35.0, 3400.0, 250.0), - (50.0, 1800.0, 80.0), - ] + return [(0., 1600., 70.), (8., 3200., 220.), (20., 2500., 130.), + (35., 3400., 250.), (50., 1800., 80.)] if profile_name == "经济巡航": - return [ - (0.0, 1500.0, 60.0), - (15.0, 2100.0, 95.0), - (35.0, 2300.0, 105.0), - (55.0, 2000.0, 90.0), - ] - return [ - (0.0, 1500.0, 50.0), - (10.0, 3000.0, 200.0), - (30.0, 2800.0, 150.0), - (50.0, 1800.0, 60.0), - ] + return [(0., 1500., 60.), (15., 2100., 95.), (35., 2300., 105.), + (55., 2000., 90.)] + return [(0., 1500., 50.), (10., 3000., 200.), (30., 2800., 150.), + (50., 1800., 60.)] -def _target_from_profile(t, points, rpm_scale, load_scale): +def _target_from_profile(t, points): rpm, torque = points[0][1], points[0][2] for p_t, p_rpm, p_torque in points: if t >= p_t: rpm, torque = p_rpm, p_torque else: break - return rpm * rpm_scale, torque * load_scale + return rpm, torque -def run_case_demo(sim_time_s, dt, initial_soc_pct, initial_engine_power_kw, profile_name, rpm_scale, load_scale): +# ============================================================ +# 阶段三:能量管理策略设计 +# ============================================================ +def run_hybrid_demo(sim_time_s, dt, initial_soc_pct, initial_engine_power_kw, + profile_name, + eng_controller_type, eng_kp, eng_ki, eng_kd, + eng_mpc_horizon, eng_mpc_W_power, eng_mpc_W_dcost, eng_mpc_overshoot, + mot_controller_type, mot_kp, mot_ki, mot_kd, mot_J, + mot_mpc_W_speed, mot_mpc_W_dcost, mot_mpc_overshoot, + soc_target_pct, soc_low_pct, soc_high_pct, + p_eng_min, p_eng_max, p_charge, k_soc, + power_reserve_pct, battery_capacity_kwh, + progress=None): + """混动系统能量管理策略仿真 (规则 + 滞环)""" try: - try: - # ===== 新增:懒加载混动系统模型,便于捕获缺失依赖 ===== - from series_hybrid_sim import SeriesHybridSystem - except ModuleNotFoundError as e: - if getattr(e, "name", "") == "torch": - return None, "算例仿真失败:缺少依赖 torch,请先在当前环境安装 PyTorch。", [] - return None, f"算例仿真失败:缺少依赖 {e.name}。", [] + import torch + from lightweight_model import EngineNNProxy + from motor_sim import MotorSim + from battery_sim import BatterySim - sim_time_s = float(np.clip(sim_time_s, 10.0, 240.0)) + # 参数裁剪 + sim_time_s = float(np.clip(sim_time_s, 10, 240)) dt = float(np.clip(dt, 0.01, 0.2)) - initial_soc_pct = float(np.clip(initial_soc_pct, 10.0, 95.0)) - initial_engine_power_kw = float(np.clip(initial_engine_power_kw, 20.0, 260.0)) - rpm_scale = float(np.clip(rpm_scale, 0.5, 1.6)) - load_scale = float(np.clip(load_scale, 0.5, 1.6)) + initial_soc_pct = float(np.clip(initial_soc_pct, 10, 95)) + initial_engine_power_kw = float(np.clip(initial_engine_power_kw, 20, 260)) + mot_J = float(np.clip(mot_J, 0.1, 10.0)) + soc_target = float(np.clip(soc_target_pct, 20, 80)) / 100.0 + soc_low = float(np.clip(soc_low_pct, 10, 60)) / 100.0 + soc_high = float(np.clip(soc_high_pct, 50, 95)) / 100.0 + if soc_low >= soc_high: + soc_high = soc_low + 0.1 + p_eng_min = float(np.clip(p_eng_min, 10, 100)) + p_eng_max = float(np.clip(p_eng_max, 100, 350)) + if p_eng_min >= p_eng_max: + p_eng_min = p_eng_max * 0.1 + p_charge = float(np.clip(p_charge, 50, 300)) + k_soc = float(np.clip(k_soc, 0, 500)) + power_reserve_pct = float(np.clip(power_reserve_pct, 0, 50)) + battery_capacity_kwh = float(np.clip(battery_capacity_kwh, 10, 200)) + + # ---- 发动机 ---- + nn_pth = os.path.join(MODEL_DATA_PATH, "engine_nn_proxy.pth") + if not os.path.exists(nn_pth): + return None, "Error: engine_nn_proxy.pth not found.", [] + + engine_nn = EngineNNProxy() + engine_nn.load_state_dict(torch.load(nn_pth, map_location='cpu')) + engine_nn.eval() + tau_fuel, K_inertia = 0.15, 100.0 + + def _bisect(func, a, b, tol=1e-4, maxiter=50): + fa, fb = func(a), func(b) + if fa * fb > 0: + return a if abs(fa) < abs(fb) else b + for _ in range(maxiter): + c = (a + b) / 2.0 + fc = func(c) + if abs(fc) < tol or (b - a) / 2 < tol: + return c + if fa * fc < 0: + b, fb = c, fc + else: + a, fa = c, fc + return (a + b) / 2.0 + + def _solve_rpm(target_p): + def obj(n): + with torch.no_grad(): + return engine_nn(torch.tensor([[0.,0.,n]], dtype=torch.float32)).numpy()[0,1] - target_p + return _bisect(obj, 1000, 58000) + + eng_N = _solve_rpm(initial_engine_power_kw) + with torch.no_grad(): + pred_init = engine_nn(torch.tensor([[0.,0.,eng_N]], dtype=torch.float32)).numpy() + eng_Wf_act = pred_init[0, 0] + eng_Wf_cmd = eng_Wf_act + + eng_use_mpc = (eng_controller_type == "MPC") + if eng_use_mpc: + from mpc_controller import TurboShaftMPCController + eng_mpc = TurboShaftMPCController( + tau_fuel=tau_fuel, K_inertia=K_inertia, dt=dt, + horizon=int(np.clip(eng_mpc_horizon, 3, 30)), + overshoot_limit=float(np.clip(eng_mpc_overshoot, 0.01, 0.30)) + ) + eng_mpc.W_power = float(np.clip(eng_mpc_W_power, 1, 1000)) + eng_mpc.W_dcost = float(np.clip(eng_mpc_W_dcost, 0.01, 50)) + eng_mpc.reset(initial_output=eng_Wf_cmd, initial_N=eng_N) + else: + from increPID import IncrementalPIDController + eng_kp = float(np.clip(eng_kp, 0.01, 30)) + eng_ki = float(np.clip(eng_ki, 0.0, 30)) + eng_kd = float(np.clip(eng_kd, 0.0, 10)) + eng_pid = IncrementalPIDController( + kp=eng_kp, ki=eng_ki, kd=eng_kd, dt=dt, + output_min=5.0, output_max=400.0, + input_scale=300.0, output_scale=400.0 + ) + eng_pid.reset(initial_output=eng_Wf_act) + + # ---- 电机 ---- + mot_use_mpc = (mot_controller_type == "MPC") + if mot_use_mpc: + drive_motor = MotorSim( + P_rate=300e3, w_rate=575.95, J=mot_J, + mpc_W_speed=float(np.clip(mot_mpc_W_speed, 1, 500)), + mpc_W_dcost=float(np.clip(mot_mpc_W_dcost, 0.01, 50)), + mpc_overshoot_limit=float(np.clip(mot_mpc_overshoot, 0.01, 0.30)), + ) + else: + drive_motor = MotorSim( + P_rate=300e3, w_rate=575.95, J=mot_J, + mpc_W_speed=0., mpc_W_dcost=0., mpc_overshoot_limit=0.05, + ) + mot_kp = float(np.clip(mot_kp, 0.01, 80)) + mot_ki = float(np.clip(mot_ki, 0.0, 150)) + mot_kd = float(np.clip(mot_kd, 0.0, 10)) + from increPID import IncrementalPIDController + w_rate = 575.95; tau_rate = 300e3 / w_rate + mot_pid = IncrementalPIDController( + kp=mot_kp, ki=mot_ki, kd=mot_kd, dt=dt, + output_min=-tau_rate, output_max=tau_rate, + input_scale=w_rate, output_scale=tau_rate + ) + mot_pid.reset(0.0) + + battery = BatterySim(capacity_kwh=battery_capacity_kwh, + initial_soc=initial_soc_pct / 100.0) + bus_voltage = battery._get_ocv(battery.SOC) + motor_actual_power_kw = 0.0 + charge_mode = (initial_soc_pct / 100.0) < soc_low + power_reserve = p_eng_max * power_reserve_pct / 100.0 points = _profile_points(profile_name) - system = SeriesHybridSystem() - system.battery.SOC = initial_soc_pct / 100.0 - system.bus_voltage = system.battery._get_ocv(system.battery.SOC) - system.genset.set_steady_state_by_power(H_env=0.0, Ma_env=0.0, Power_target=initial_engine_power_kw) + time_array = np.arange(0, sim_time_s, dt) - time_array = np.arange(0.0, sim_time_s, dt) log = {k: [] for k in [ - "soc", "bus_voltage", "prop_speed_rpm", "target_prop_rpm", - "target_engine_pwr", "p_engine_out_kw", "p_drive_req_kw", - "p_batt_actual_kw", "wf_kg_h" + 'soc', 'bus_voltage', 'prop_speed_rpm', 'target_prop_rpm', + 'target_engine_pwr', 'p_engine_out_kw', 'p_drive_req_kw', + 'p_batt_actual_kw', 'wf_kg_h', 'ems_mode' ]} - for t in time_array: - target_rpm, load_torque = _target_from_profile(t, points, rpm_scale, load_scale) - res = system.step(dt, target_rpm, load_torque) - res["target_prop_rpm"] = target_rpm - for k in log: - log[k].append(res[k]) + n_steps = len(time_array) + for step_i, t in enumerate(time_array): + if progress is not None and step_i % max(1, n_steps // 20) == 0: + progress(step_i / n_steps, desc=f"混动仿真 {step_i}/{n_steps} (t={t:.1f}s)") + target_rpm, load_torque_t = _target_from_profile(t, points) - speed_error = np.array(log["target_prop_rpm"]) - np.array(log["prop_speed_rpm"]) - soc_arr = np.array(log["soc"]) - fuel_arr = np.array(log["wf_kg_h"]) - engine_pwr_arr = np.array(log["p_engine_out_kw"]) - batt_pwr_arr = np.array(log["p_batt_actual_kw"]) + # Motor step + if not mot_use_mpc: + w_set_rad = target_rpm * 2 * np.pi / 60.0 + T_cmd = mot_pid.compute(setpoint=w_set_rad, measurement=drive_motor.w_M) + p_cmd_kw = -T_cmd * max(abs(drive_motor.w_M), 1.0) / 1000.0 + motor_state = drive_motor.step(dt=dt, n_setpoint=target_rpm, + p_bus_actual_kw=p_cmd_kw, v_bus=bus_voltage, + t_load=load_torque_t, t_ext=0.0) + else: + motor_state = drive_motor.step(dt=dt, n_setpoint=target_rpm, + p_bus_actual_kw=motor_actual_power_kw, v_bus=bus_voltage, + t_load=load_torque_t, t_ext=0.0) - fig, axes = plt.subplots(3, 1, figsize=(12, 10), sharex=True) - axes[0].plot(time_array, log["target_prop_rpm"], "k--", lw=1.5, label="目标转速") - axes[0].plot(time_array, log["prop_speed_rpm"], "b-", lw=1.5, label="实际转速") - axes[0].set_ylabel("RPM") - axes[0].set_title("推进轴转速响应") - axes[0].grid(True, linestyle=":") - axes[0].legend() + p_drive_req = motor_state['p_bus_req_kw'] + actual_rpm = motor_state['n_rpm'] - axes[1].plot(time_array, log["p_drive_req_kw"], "k--", lw=1.2, label="电机需求") - axes[1].plot(time_array, log["p_engine_out_kw"], "r-", lw=1.2, label="发动机输出") - axes[1].plot(time_array, log["p_batt_actual_kw"], "g-", lw=1.2, label="电池功率") - axes[1].axhline(0, color="gray", lw=1) - axes[1].set_ylabel("kW") - axes[1].set_title("功率分配") - axes[1].grid(True, linestyle=":") - axes[1].legend() + # EMS + soc = battery.SOC + if soc < soc_low: charge_mode = True + elif soc > soc_high: charge_mode = False - axes[2].plot(time_array, log["bus_voltage"], "m-", lw=1.2, label="母线电压") - axes[2].set_ylabel("V") - axes[2].set_xlabel("时间 (s)") - axes[2].set_title("电气状态") - axes[2].grid(True, linestyle=":") + if soc < 0.10: + target_engine_pwr = p_eng_max; ems_mode_str = "Emergency Charge" + elif soc > 0.95: + target_engine_pwr = p_eng_min; ems_mode_str = "Overcharge Prot." + elif charge_mode: + target_engine_pwr = p_charge; ems_mode_str = "Charge Mode" + else: + soc_error = soc_target - soc + target_engine_pwr = p_drive_req + power_reserve + soc_error * k_soc + ems_mode_str = "Power Follow" + target_engine_pwr = float(np.clip(target_engine_pwr, p_eng_min, p_eng_max)) + + # Engine step + batch_inp = torch.tensor([[0.,0.,eng_N],[0.,0.,eng_N+5.]], dtype=torch.float32) + with torch.no_grad(): + bp = engine_nn(batch_inp).numpy() + Wf_req, P_eng_out = bp[0,0], bp[0,1] + + if eng_use_mpc: + k_wf = (bp[1,0]-bp[0,0])/5.0; k_p = (bp[1,1]-bp[0,1])/5.0 + eng_Wf_cmd = eng_mpc.compute(current_N=eng_N, current_Wfact=eng_Wf_act, + target_power=target_engine_pwr, precalc_params=(Wf_req, P_eng_out, k_wf, k_p)) + else: + eng_Wf_cmd = eng_pid.compute(setpoint=target_engine_pwr, measurement=P_eng_out) + + eng_Wf_act += (eng_Wf_cmd - eng_Wf_act) / tau_fuel * dt + eng_N += K_inertia * (eng_Wf_act - Wf_req) * dt + + # Battery + p_batt_req = p_drive_req - P_eng_out + p_batt_actual, v_bus, i_batt, soc_new = battery.step(dt, p_batt_req) + bus_voltage = v_bus + motor_actual_power_kw = P_eng_out + p_batt_actual + + log['soc'].append(soc_new * 100.0) + log['bus_voltage'].append(v_bus) + log['prop_speed_rpm'].append(actual_rpm) + log['target_prop_rpm'].append(target_rpm) + log['target_engine_pwr'].append(target_engine_pwr) + log['p_engine_out_kw'].append(P_eng_out) + log['p_drive_req_kw'].append(p_drive_req) + log['p_batt_actual_kw'].append(p_batt_actual) + log['wf_kg_h'].append(eng_Wf_act) + log['ems_mode'].append(ems_mode_str) + + if progress is not None: + progress(1.0, desc="绘图中...") + # ---- Plots (English) ---- + soc_arr = np.array(log['soc']) + speed_error = np.array(log['target_prop_rpm']) - np.array(log['prop_speed_rpm']) + + fig, axes = plt.subplots(4, 1, figsize=(12, 14), sharex=True) + fig.suptitle('Hybrid EMS Validation (Rule-Based + Hysteresis)', + fontweight='bold', fontsize=13) + + mode_colors = {'Power Follow': '#E3F2FD', 'Charge Mode': '#FFEBEE', + 'Emergency Charge': '#FFCDD2', 'Overcharge Prot.': '#E8F5E9'} + + axes[0].plot(time_array, log['target_prop_rpm'], 'k--', lw=1.5, label='Target') + axes[0].plot(time_array, log['prop_speed_rpm'], 'b-', lw=1.5, label='Actual') + axes[0].set_ylabel('Speed (RPM)'); axes[0].set_title('Propulsion Speed') + axes[0].grid(True, linestyle=':'); axes[0].legend() + + modes = log['ems_mode'] + i = 0; added = set() + while i < len(modes): + m = modes[i]; j = i + while j < len(modes) and modes[j] == m: j += 1 + col = mode_colors.get(m, '#F5F5F5') + lbl = m if m not in added else None + axes[1].axvspan(time_array[i], time_array[min(j-1, len(time_array)-1)], + alpha=0.3, color=col, label=lbl) + if lbl: added.add(m) + i = j + + axes[1].plot(time_array, log['p_drive_req_kw'], 'k--', lw=1.2, label='Motor Demand') + axes[1].plot(time_array, log['target_engine_pwr'], color='darkred', ls=':', lw=1, label='Eng Target') + axes[1].plot(time_array, log['p_engine_out_kw'], 'r-', lw=1.2, label='Eng Output') + axes[1].plot(time_array, log['p_batt_actual_kw'], 'g-', lw=1.2, label='Battery') + axes[1].axhline(0, color='gray', lw=0.8) + axes[1].set_ylabel('Power (kW)'); axes[1].set_title('Power Allocation (bg=EMS mode)') + axes[1].grid(True, linestyle=':'); axes[1].legend(ncol=3, fontsize=8, loc='upper right') + + axes[2].plot(time_array, log['bus_voltage'], 'm-', lw=1.2, label='Bus Voltage') + axes[2].set_ylabel('Voltage (V)'); axes[2].set_title('Electrical State & SOC') + axes[2].grid(True, linestyle=':'); axes[2].legend(loc='upper left') ax_soc = axes[2].twinx() - ax_soc.plot(time_array, log["soc"], "c--", lw=1.6, label="SOC") - ax_soc.set_ylabel("SOC (%)") + ax_soc.plot(time_array, log['soc'], 'c-', lw=1.6, label='SOC') + ax_soc.axhline(soc_low*100, color='r', ls=':', lw=1, alpha=0.7, label=f'Low ({soc_low*100:.0f}%)') + ax_soc.axhline(soc_high*100, color='g', ls=':', lw=1, alpha=0.7, label=f'High ({soc_high*100:.0f}%)') + ax_soc.axhline(soc_target*100, color='b', ls='-.', lw=1, alpha=0.5, label=f'Target ({soc_target*100:.0f}%)') + ax_soc.set_ylabel('SOC (%)'); ax_soc.legend(loc='upper right', fontsize=8) - fig.tight_layout() + axes[3].plot(time_array, log['wf_kg_h'], 'tab:orange', lw=1.2, label='Fuel Flow') + axes[3].set_ylabel('Fuel (kg/h)'); axes[3].set_xlabel('Time (s)') + axes[3].set_title('Fuel Consumption') + axes[3].grid(True, linestyle=':'); axes[3].legend() + + fig.tight_layout(rect=[0, 0, 1, 0.96]) + + mode_times = {} + for m_ in modes: mode_times[m_] = mode_times.get(m_, 0) + dt + mode_str = ', '.join([f'{k}: {v:.1f}s' for k, v in mode_times.items()]) + + fuel_arr = np.array(log['wf_kg_h']) + engine_pwr_arr = np.array(log['p_engine_out_kw']) + batt_pwr_arr = np.array(log['p_batt_actual_kw']) summary = ( - f"### 算例结果解读\n" - f"- 仿真时长:{sim_time_s:.1f} s,步长:{dt:.3f} s\n" - f"- 最大转速误差:{np.max(np.abs(speed_error)):.1f} RPM\n" - f"- SOC 变化:{soc_arr[0]:.2f}% → {soc_arr[-1]:.2f}%(最小 {np.min(soc_arr):.2f}%)\n" - f"- 平均发动机输出:{np.mean(engine_pwr_arr):.2f} kW\n" - f"- 平均电池功率:{np.mean(batt_pwr_arr):.2f} kW\n" - f"- 平均燃油流量:{np.mean(fuel_arr):.2f} kg/h" + f"### Hybrid Simulation Summary\n" + f"- **Duration**: {sim_time_s:.0f}s, dt={dt:.3f}s\n" + f"- **Controllers**: Engine={eng_controller_type}, Motor={mot_controller_type}\n" + f"- **Max Speed Error**: {np.max(np.abs(speed_error)):.1f} RPM\n" + f"- **SOC**: {soc_arr[0]:.1f}% → {soc_arr[-1]:.1f}% " + f"(min {np.min(soc_arr):.1f}%, max {np.max(soc_arr):.1f}%)\n" + f"- **Avg Engine Power**: {np.mean(engine_pwr_arr):.1f} kW\n" + f"- **Avg Battery**: {np.mean(batt_pwr_arr):.1f} kW (+discharge/−charge)\n" + f"- **Avg Fuel**: {np.mean(fuel_arr):.1f} kg/h\n" + f"- **EMS Modes**: {mode_str}" ) - pick_idx = np.linspace(0, len(time_array) - 1, 8, dtype=int) - table_data = [] - for idx in pick_idx: - table_data.append([ - round(float(time_array[idx]), 2), - round(float(log["target_prop_rpm"][idx]), 1), - round(float(log["prop_speed_rpm"][idx]), 1), - round(float(log["p_engine_out_kw"][idx]), 2), - round(float(log["p_batt_actual_kw"][idx]), 2), - round(float(log["soc"][idx]), 2), - ]) + pick_idx = np.linspace(0, len(time_array)-1, 8, dtype=int) + table_data = [[ + round(float(time_array[idx]),2), + round(float(log['target_prop_rpm'][idx]),1), + round(float(log['prop_speed_rpm'][idx]),1), + round(float(log['p_engine_out_kw'][idx]),2), + round(float(log['p_batt_actual_kw'][idx]),2), + round(float(log['soc'][idx]),2), + log['ems_mode'][idx], + ] for idx in pick_idx] return fig, summary, table_data + except Exception as e: - return None, f"算例仿真失败:{e}", [] + import traceback + return None, f"Hybrid simulation failed: {e}\n```\n{traceback.format_exc()}\n```", [] diff --git a/data_usage/usage_stats.json b/data_usage/usage_stats.json index 47c26cf..9e8f16e 100644 --- a/data_usage/usage_stats.json +++ b/data_usage/usage_stats.json @@ -1,4 +1,4 @@ { - "total_users": 22, - "last_saved_at": 1775496284.1673155 + "total_users": 37, + "last_saved_at": 1775560914.9575145 } \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index a36ea6f..d0d6784 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ # requirements.txt -# ===== 新增:PyTorch CPU 轮子下载源 ===== ---extra-index-url https://download.pytorch.org/whl/cpu +# ===== PyTorch:GPU (CUDA 12.1) 版本,如无 NVIDIA GPU 可改为 cpu ===== +--extra-index-url https://download.pytorch.org/whl/cu121 gradio==4.44.1 gradio-client==1.3.0 @@ -14,10 +14,11 @@ matplotlib==3.9.4 aiohttp==3.13.5 pillow==10.4.0 # ===== 新增:混动模型(Model)运行依赖 ===== -torch==2.4.1 +torch==2.4.1+cu121 botorch==0.14.0 gpytorch==1.14 pyro-ppl==1.9.1 pandas==2.3.3 -scipy==1.15.3 +scipy>=1.10 scikit-learn==1.7.1 +psutil>=5.9 diff --git a/ui_components.py b/ui_components.py index 25af302..f4d7a96 100644 --- a/ui_components.py +++ b/ui_components.py @@ -1,8 +1,13 @@ -import gradio as gr +import gradio as gr from assets.knowledge_cards_html import ( TIME_DOMAIN_KNOWLEDGE, FREQUENCY_DOMAIN_KNOWLEDGE, - ROOT_LOCUS_KNOWLEDGE + ROOT_LOCUS_KNOWLEDGE, + ENGINE_CONTROL_KNOWLEDGE, + MOTOR_CONTROL_KNOWLEDGE, + GPR_KNOWLEDGE, + NN_KNOWLEDGE, + EMS_KNOWLEDGE, ) def create_header(): @@ -66,6 +71,20 @@ def create_time_domain_tab(): ui_dict = {} with gr.Row(): with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
📊 传递函数设定
") + ui_dict["num_input"] = gr.Textbox( + label="分子系数 (Numerator)", + value="1", + placeholder="例如: 1 或 1,2,3", + info="💡 用逗号分隔,从最高次项到常数项" + ) + ui_dict["den_input"] = gr.Textbox( + label="分母系数 (Denominator)", + value="1,6,11,6", + placeholder="例如: 1,2,1", + info="💡 分母阶数通常 ≥ 分子阶数" + ) with gr.Group(): gr.HTML("
🔧 系统模型
") ui_dict["tf_display"] = gr.Markdown(label="当前传递函数", elem_classes="output-display") @@ -116,6 +135,20 @@ def create_frequency_domain_tab(): ui_dict = {} with gr.Row(): with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
📊 传递函数设定
") + ui_dict["num_input"] = gr.Textbox( + label="分子系数 (Numerator)", + value="1", + placeholder="例如: 1 或 1,2,3", + info="💡 用逗号分隔,从最高次项到常数项" + ) + ui_dict["den_input"] = gr.Textbox( + label="分母系数 (Denominator)", + value="1,6,11,6", + placeholder="例如: 1,2,1", + info="💡 分母阶数通常 ≥ 分子阶数" + ) with gr.Group(): gr.HTML("
🎚️ 调整系统增益
") ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块查看实时变化") @@ -127,6 +160,8 @@ def create_frequency_domain_tab(): gr.HTML("
📊 稳定裕度分析
") ui_dict["metrics_display"] = gr.Textbox(label="Stability Margins", lines=4, interactive=False, elem_classes="output-metrics") ui_dict["stability_display"] = gr.Markdown(elem_classes="stability-result") + with gr.Row(): + ui_dict["analyze_button"] = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn") with gr.Column(scale=2): ui_dict["plot_output"] = gr.Plot(label="频域响应图", elem_classes="plot-container") # 知识卡片 @@ -163,6 +198,20 @@ def create_root_locus_tab(): ui_dict = {} with gr.Row(): with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
📊 传递函数设定
") + ui_dict["num_input"] = gr.Textbox( + label="分子系数 (Numerator)", + value="1", + placeholder="例如: 1 或 1,2,3", + info="💡 用逗号分隔,从最高次项到常数项" + ) + ui_dict["den_input"] = gr.Textbox( + label="分母系数 (Denominator)", + value="1,6,11,6", + placeholder="例如: 1,2,1", + info="💡 分母阶数通常 ≥ 分子阶数" + ) with gr.Group(): gr.HTML("
🎚️ 调整系统增益
") ui_dict["log_k_slider"] = gr.Slider(minimum=-4, maximum=4, value=1, step=0.01, label="对数增益 log₁₀(K)", info="💡 拖动滑块观察极点移动") @@ -170,6 +219,8 @@ def create_root_locus_tab(): with gr.Group(): gr.HTML("
📍 闭环极点位置
") ui_dict["poles_display"] = gr.Textbox(label="Closed-Loop Pole Locations", lines=6, interactive=False, elem_classes="output-metrics") + with gr.Row(): + ui_dict["analyze_button"] = gr.Button("🚀 开始分析", variant="primary", scale=1, elem_classes="primary-btn") with gr.Column(scale=2): ui_dict["plot_output"] = gr.Plot(label="根轨迹图") gr.HTML(f""" @@ -201,34 +252,285 @@ def create_root_locus_tab(): return ui_dict def create_case_demo_tab(): - """创建算例演示选项卡的UI组件""" + """创建算例演示选项卡 — 四阶段交互设计(蒸馏→发动机→电机→能量管理)""" ui_dict = {} - with gr.Row(): - with gr.Column(scale=1): - with gr.Group(): - gr.HTML("
🧪 算例参数设置
") - ui_dict["profile"] = gr.Dropdown( - choices=["起飞-巡航-降落", "高机动阶跃", "经济巡航"], - value="起飞-巡航-降落", - label="工况模板" - ) - ui_dict["sim_time"] = gr.Slider(minimum=20, maximum=180, value=60, step=5, label="仿真时长 (s)") - ui_dict["dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02, label="仿真步长 (s)") - ui_dict["initial_soc"] = gr.Slider(minimum=20, maximum=90, value=60, step=1, label="初始SOC (%)") - ui_dict["initial_engine_power"] = gr.Slider(minimum=20, maximum=250, value=50, step=5, label="初始发动机功率 (kW)") - ui_dict["rpm_scale"] = gr.Slider(minimum=0.6, maximum=1.4, value=1.0, step=0.05, label="目标转速缩放系数") - ui_dict["load_scale"] = gr.Slider(minimum=0.6, maximum=1.4, value=1.0, step=0.05, label="负载转矩缩放系数") - ui_dict["run_button"] = gr.Button("🚀 运行混动算例", variant="primary", elem_classes="primary-btn") - with gr.Group(): - gr.HTML("
📝 结果解读
") - ui_dict["summary"] = gr.Markdown() - with gr.Column(scale=2): - ui_dict["plot"] = gr.Plot(label="混动系统响应图") - ui_dict["table"] = gr.Dataframe( - headers=["时间(s)", "目标转速", "实际转速", "发动机功率(kW)", "电池功率(kW)", "SOC(%)"], - label="关键时刻数据", - interactive=False + + # === MathJax re-render helper (reused across tabs) === + def _mathjax_script(div_id): + return f""" + """ + + with gr.Tabs(): + # ========== 阶段零:模型训练(GPR + NN 两个子标签页)========== + with gr.TabItem("🧬 模型训练", id="distill_tab"): + gr.HTML("""
+ 阶段零:模型训练包含两步——先训练/验证 GPR 高斯过程代理模型, + 再将其知识蒸馏为轻量 NN(MLP)用于后续实时控制仿真。
""") + with gr.Tabs(): + # ----- 子标签页 A: GPR 模型训练 ----- + with gr.TabItem("📈 GPR 模型训练", id="gpr_sub_tab"): + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
🔬 GPR 训练设置
") + ui_dict["gpr_mode"] = gr.Radio( + choices=["load", "train"], value="load", + label="运行模式", + info="load: 加载已有权重 | train: 从头训练(需 botorch)") + ui_dict["gpr_run_button"] = gr.Button( + "🚀 运行 GPR 训练 / 加载", variant="primary", + elem_classes="primary-btn") + with gr.Group(): + gr.HTML("
📝 GPR 结果
") + ui_dict["gpr_summary"] = gr.Markdown() + with gr.Column(scale=2): + ui_dict["gpr_plot"] = gr.Plot(label="GPR 模型结果") + gr.HTML(f""" +
+ {GPR_KNOWLEDGE} +
+ {_mathjax_script('gpr-knowledge')} + """) + + # ----- 子标签页 B: NN 模型训练 ----- + with gr.TabItem("🧠 NN 模型训练", id="nn_sub_tab"): + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
🧪 NN 训练参数
") + ui_dict["distill_epochs"] = gr.Slider(minimum=500, maximum=8000, value=3000, step=100, + label="训练轮数 (Epochs)", info="越多越精确,但耗时更长") + ui_dict["distill_lr"] = gr.Slider(minimum=1e-4, maximum=1e-2, value=3e-3, step=1e-4, + label="学习率 (LR)", info="推荐 1e-3 ~ 5e-3") + ui_dict["distill_hidden"] = gr.Slider(minimum=16, maximum=256, value=64, step=16, + label="隐藏层宽度", info="MLP每层神经元数") + ui_dict["distill_run_button"] = gr.Button("🚀 开始 NN 训练", variant="primary", + elem_classes="primary-btn") + with gr.Group(): + gr.HTML("
📝 NN 训练结果
") + ui_dict["distill_summary"] = gr.Markdown() + with gr.Column(scale=2): + ui_dict["distill_plot"] = gr.Plot(label="NN 训练结果 (Loss + Parity)") + gr.HTML(f""" +
+ {NN_KNOWLEDGE} +
+ {_mathjax_script('nn-knowledge')} + """) + + # ========== 阶段一:发动机控制器设计 ========== + with gr.TabItem("🔧 发动机控制器设计", id="engine_tab"): + gr.HTML("""
+ 阶段一:选择 PID 或 MPC 控制器,调整参数,运行阶跃响应测试,观察功率跟踪性能。
""") + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
🎯 控制器选择
") + ui_dict["eng_controller_type"] = gr.Radio( + choices=["PID", "MPC"], value="PID", label="控制器类型", + info="PID: 经典三参数 | MPC: 模型预测控制") + with gr.Group(visible=True) as eng_pid_group: + gr.HTML("
🎛️ PID 参数
") + ui_dict["eng_kp"] = gr.Slider(minimum=0.1, maximum=20, value=4.652, step=0.01, + label="比例增益 Kp", info="增大加快响应,过大导致振荡") + ui_dict["eng_ki"] = gr.Slider(minimum=0.0, maximum=20, value=7.078, step=0.01, + label="积分增益 Ki", info="消除稳态误差,过大导致超调") + ui_dict["eng_kd"] = gr.Slider(minimum=0.0, maximum=5, value=0.222, step=0.001, + label="微分增益 Kd", info="抑制振荡,改善动态特性") + ui_dict["eng_pid_group"] = eng_pid_group + with gr.Group(visible=False) as eng_mpc_group: + gr.HTML("
🎛️ MPC 参数
") + ui_dict["eng_mpc_horizon"] = gr.Slider(minimum=3, maximum=30, value=15, step=1, + label="预测时域 (Horizon)", info="MPC前看步数") + ui_dict["eng_mpc_W_power"] = gr.Slider(minimum=1, maximum=500, value=200, step=1, + label="功率跟踪权重 W_power") + ui_dict["eng_mpc_W_dcost"] = gr.Slider(minimum=0.01, maximum=20, value=1.5, step=0.01, + label="控制增量权重 W_Δcost") + ui_dict["eng_mpc_overshoot"] = gr.Slider(minimum=1, maximum=30, value=5, step=1, + label="超调限制 (%)") + ui_dict["eng_mpc_group"] = eng_mpc_group + with gr.Group(): + gr.HTML("
⚙️ 发动机模型参数
") + ui_dict["eng_tau_fuel"] = gr.Slider(minimum=0.05, maximum=1.0, value=0.15, step=0.01, + label="燃油执行机构时间常数 τ (s)") + ui_dict["eng_K_inertia"] = gr.Slider(minimum=10, maximum=500, value=100, step=5, + label="转子惯性增益 K") + with gr.Group(): + gr.HTML("
🧪 仿真设置
") + ui_dict["eng_sim_time"] = gr.Slider(minimum=5, maximum=60, value=30, step=1, + label="仿真时长 (s)") + ui_dict["eng_dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02, + label="仿真步长 (s)") + ui_dict["eng_init_power"] = gr.Slider(minimum=20, maximum=250, value=100, step=5, + label="初始功率 (kW)") + ui_dict["eng_target_power"] = gr.Slider(minimum=20, maximum=300, value=200, step=5, + label="目标功率 (kW)") + ui_dict["eng_run_button"] = gr.Button("🚀 运行发动机仿真", variant="primary", + elem_classes="primary-btn") + with gr.Group(): + gr.HTML("
📝 设计结果
") + ui_dict["eng_summary"] = gr.Markdown() + with gr.Column(scale=2): + ui_dict["eng_plot"] = gr.Plot(label="发动机控制器阶跃响应") + gr.HTML(f""" +
+ {ENGINE_CONTROL_KNOWLEDGE} +
+ {_mathjax_script('engine-knowledge')} + """) + + # Radio toggle PID/MPC visibility + ui_dict["eng_controller_type"].change( + fn=lambda ct: (gr.update(visible=(ct == "PID")), gr.update(visible=(ct == "MPC"))), + inputs=[ui_dict["eng_controller_type"]], + outputs=[eng_pid_group, eng_mpc_group], ) + + # ========== 阶段二:电机控制器设计 ========== + with gr.TabItem("⚡ 电机控制器设计", id="motor_tab"): + gr.HTML("""
+ 阶段二:选择 PID 或 MPC 控制器,运行转速跟踪 + 负载扰动测试。 + 在仿真60%时刻自动施加50%负载扰动,检验抗扰能力。
""") + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
🎯 控制器选择
") + ui_dict["mot_controller_type"] = gr.Radio( + choices=["PID", "MPC"], value="PID", label="控制器类型", + info="PID: 经典三参数 | MPC: 模型预测控制") + with gr.Group(visible=True) as mot_pid_group: + gr.HTML("
🎛️ PID 参数
") + ui_dict["mot_kp"] = gr.Slider(minimum=0.1, maximum=50, value=5.0, step=0.1, + label="比例增益 Kp", info="增大加快转速响应") + ui_dict["mot_ki"] = gr.Slider(minimum=0.0, maximum=100, value=2.0, step=0.1, + label="积分增益 Ki", info="消除转速稳态偏差") + ui_dict["mot_kd"] = gr.Slider(minimum=0.0, maximum=5, value=0.5, step=0.01, + label="微分增益 Kd", info="抑制转速振荡") + ui_dict["mot_pid_group"] = mot_pid_group + with gr.Group(visible=False) as mot_mpc_group: + gr.HTML("
🎛️ MPC 参数
") + ui_dict["mot_mpc_W_speed"] = gr.Slider(minimum=1, maximum=500, value=200, step=1, + label="转速跟踪权重 W_speed") + ui_dict["mot_mpc_W_dcost"] = gr.Slider(minimum=0.01, maximum=20, value=0.3, step=0.01, + label="控制增量权重 W_Δcost") + ui_dict["mot_mpc_overshoot"] = gr.Slider(minimum=1, maximum=30, value=5, step=1, + label="超调限制 (%)") + ui_dict["mot_mpc_group"] = mot_mpc_group + with gr.Group(): + gr.HTML("
⚙️ 电机模型参数
") + ui_dict["mot_J"] = gr.Slider(minimum=0.1, maximum=5.0, value=1.0, step=0.1, + label="转动惯量 J (kg·m²)", info="越大响应越慢但越平稳") + with gr.Group(): + gr.HTML("
🧪 仿真设置
") + ui_dict["mot_sim_time"] = gr.Slider(minimum=5, maximum=60, value=30, step=1, + label="仿真时长 (s)") + ui_dict["mot_dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02, + label="仿真步长 (s)") + ui_dict["mot_target_rpm"] = gr.Slider(minimum=500, maximum=5000, value=2000, step=50, + label="目标转速 (RPM)") + ui_dict["mot_load_torque"] = gr.Slider(minimum=10, maximum=400, value=80, step=5, + label="负载转矩 (Nm)") + ui_dict["mot_run_button"] = gr.Button("🚀 运行电机仿真", variant="primary", + elem_classes="primary-btn") + with gr.Group(): + gr.HTML("
📝 设计结果
") + ui_dict["mot_summary"] = gr.Markdown() + with gr.Column(scale=2): + ui_dict["mot_plot"] = gr.Plot(label="电机控制器阶跃响应") + gr.HTML(f""" +
+ {MOTOR_CONTROL_KNOWLEDGE} +
+ {_mathjax_script('motor-knowledge')} + """) + + # Radio toggle PID/MPC visibility + ui_dict["mot_controller_type"].change( + fn=lambda ct: (gr.update(visible=(ct == "PID")), gr.update(visible=(ct == "MPC"))), + inputs=[ui_dict["mot_controller_type"]], + outputs=[mot_pid_group, mot_mpc_group], + ) + + # ========== 阶段三:能量管理策略设计 ========== + with gr.TabItem("🔋 能量管理策略设计", id="ems_tab"): + gr.HTML("""
+ 阶段三:设计基于规则的能量管理策略(自动引用前两阶段的控制器参数)。
+ 策略原理:SOC < 下限阈值 → 进入充电模式; + SOC > 上限阈值 → 退出充电,进入功率跟随模式。 + 下限~上限之间为滞环区间,防止模式频繁切换。
""") + with gr.Row(): + with gr.Column(scale=1): + with gr.Group(): + gr.HTML("
📊 SOC规则参数(滞环控制)
") + ui_dict["soc_target"] = gr.Slider(minimum=30, maximum=80, value=60, step=1, + label="SOC目标值 (%)", info="功率跟随模式下的SOC补偿基准") + ui_dict["soc_low"] = gr.Slider(minimum=15, maximum=50, value=30, step=1, + label="SOC下限阈值 (%)", info="低于此值→进入充电模式") + ui_dict["soc_high"] = gr.Slider(minimum=50, maximum=90, value=70, step=1, + label="SOC上限阈值 (%)", info="高于此值→退出充电模式") + with gr.Group(): + gr.HTML("
⚡ 功率规则参数
") + ui_dict["p_eng_min"] = gr.Slider(minimum=10, maximum=100, value=20, step=5, + label="发动机最小功率 (kW)") + ui_dict["p_eng_max"] = gr.Slider(minimum=100, maximum=350, value=300, step=10, + label="发动机最大功率 (kW)") + ui_dict["p_charge"] = gr.Slider(minimum=50, maximum=300, value=200, step=10, + label="充电模式发动机功率 (kW)", info="进入充电模式后发动机固定输出") + ui_dict["k_soc"] = gr.Slider(minimum=0, maximum=200, value=50, step=5, + label="SOC补偿增益 (kW/ΔSOC)", info="跟随模式下对SOC偏差的修正力度") + ui_dict["power_reserve"] = gr.Slider(minimum=0, maximum=50, value=10, step=1, + label="动态功率储备 (%)", info="发动机额外预留功率百分比") + with gr.Group(): + gr.HTML("
🔧 系统与仿真参数
") + ui_dict["battery_capacity"] = gr.Slider(minimum=10, maximum=200, value=50, step=5, + label="电池容量 (kWh)") + ui_dict["initial_soc"] = gr.Slider(minimum=10, maximum=95, value=60, step=1, + label="初始SOC (%)") + ui_dict["initial_engine_power"] = gr.Slider(minimum=20, maximum=250, value=50, step=5, + label="初始发动机功率 (kW)") + ui_dict["profile"] = gr.Dropdown( + choices=["起飞-巡航-降落", "高机动阶跃", "经济巡航"], + value="起飞-巡航-降落", label="工况模板") + ui_dict["sim_time"] = gr.Slider(minimum=20, maximum=180, value=60, step=5, + label="仿真时长 (s)") + ui_dict["dt"] = gr.Dropdown(choices=[0.02, 0.05, 0.1], value=0.02, + label="仿真步长 (s)") + ui_dict["hybrid_run_button"] = gr.Button("🚀 运行混动系统仿真", variant="primary", + size="lg", elem_classes="primary-btn") + with gr.Group(): + gr.HTML("
📝 结果摘要
") + ui_dict["hybrid_summary"] = gr.Markdown() + with gr.Column(scale=2): + ui_dict["hybrid_plot"] = gr.Plot(label="混动系统响应图") + ui_dict["hybrid_table"] = gr.Dataframe( + headers=["时间(s)", "目标转速", "实际转速", "发动机功率(kW)", + "电池功率(kW)", "SOC(%)", "EMS模式"], + label="关键时刻数据", interactive=False + ) + gr.HTML(f""" +
+ {EMS_KNOWLEDGE} +
+ {_mathjax_script('ems-knowledge')} + """) return ui_dict def create_chatbot_tab(): @@ -260,3 +562,4 @@ def create_chatbot_tab(): label="💡 试试这些问题:" ) return ui_dict + -- 2.54.0 From 0500304584a18ac91e27d4302eb41b974c7f929b Mon Sep 17 00:00:00 2001 From: Hongru Date: Tue, 7 Apr 2026 19:33:49 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E8=AF=B4=E6=98=8E=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- API_CONFIG.md | 689 ++++++++++++++++++++----- CHANGELOG.md | 413 +++++++++++++-- GUIDANCE.md | 966 ++++++++++++++++++++++++++++-------- README.md | 755 ++++++++++++++++++++-------- data_usage/usage_stats.json | 2 +- 5 files changed, 2232 insertions(+), 593 deletions(-) diff --git a/API_CONFIG.md b/API_CONFIG.md index 8d2625b..a77ec94 100644 --- a/API_CONFIG.md +++ b/API_CONFIG.md @@ -1,201 +1,640 @@ # API 配置指南 -本文档详细说明如何配置 DeepSeek 和 Gemini API。 - -## 📋 目录 - -- [DeepSeek API 配置](#deepseek-api-配置) -- [Gemini API 配置](#gemini-api-配置) -- [常见问题](#常见问题) +> 本文档详细说明如何为自动控制理论AI+数智平台配置 DeepSeek 和 Gemini API,包括密钥获取、配置方法、参数调优与常见问题排查。 --- -## 🚀 DeepSeek API 配置 +## 一、API 概述 -### 1. 获取 API 密钥 +### 1.1 平台支持的 AI API -1. 访问 [DeepSeek 平台](https://platform.deepseek.com/) -2. 注册账号并登录 -3. 进入 [API Keys 页面](https://platform.deepseek.com/api_keys) -4. 点击"创建新密钥" -5. 复制生成的 API 密钥(格式:`sk-xxxxxxxxxxxxxxxx`) +本平台目前支持以下 AI API 提供商: -### 2. 配置到应用 +| API 提供商 | 支持模型 | 访问地区 | 推荐程度 | +|------------|----------|----------|----------| +| DeepSeek | deepseek-chat / deepseek-coder | 中国大陆 | ⭐⭐⭐⭐⭐ 首选 | +| Gemini (Google) | gemini-1.5-flash / gemini-1.5-pro | 部分受限 | ⭐⭐⭐ | -编辑 `app.py` 文件的配置区域(第 7-28 行): +### 1.2 为什么需要 API + +AI 智能问答模块依赖外部大语言模型 API 来实现: +- 自动控制理论专业问题的理解和回答 +- LaTeX 数学公式的正确生成 +- 多轮对话的上下文记忆 + +### 1.3 配置优先级 + +``` +┌─────────────────────────────────────────┐ +│ 推荐配置顺序 │ +├─────────────────────────────────────────┤ +│ │ +│ 1. DeepSeek API(国内访问,速度快) │ +│ ↓ │ +│ 2. Gemini API(需要代理) │ +│ ↓ │ +│ 3. 本地部署(需高配置服务器) │ +│ │ +└─────────────────────────────────────────┘ +``` + +--- + +## 二、DeepSeek API 配置 + +### 2.1 DeepSeek 简介 + +DeepSeek 是国内领先的 AI 大模型服务提供商,提供: +- **deepseek-chat**:通用对话模型,适合教育场景 +- **deepseek-coder**:代码专用模型,适合技术问题 +- **价格优惠**:相比 OpenAI 等海外服务商更具性价比 +- **国内访问**:无需代理,网络延迟低 + +### 2.2 获取 API 密钥 + +**Step 1:访问 DeepSeek 平台** + +打开浏览器,访问:https://platform.deepseek.com/ + +**Step 2:注册/登录账号** + +- 使用手机号或邮箱注册 +- 已注册用户直接登录 + +**Step 3:进入 API Keys 管理页面** + +登录后,点击顶部导航栏的 "API Keys": +``` +https://platform.deepseek.com/api_keys +``` + +**Step 4:创建新密钥** + +1. 点击 "创建新密钥" 按钮 +2. 输入密钥名称(可自定义,如 "AutoControlCourse") +3. 点击确认 +4. **立即复制密钥**(只显示一次!) + +**密钥格式示例:** +``` +sk-2292af2428d7419897ca1fb6e99ba6bc +``` + +### 2.3 配置到项目 + +**方法一:直接编辑 config.py(最简单)** + +1. 打开项目根目录下的 `config.py` 文件 +2. 找到 API 配置区域 +3. 将 `API_KEY` 替换为您的密钥 ```python # ==================== API 配置 ==================== -API_KEY = "sk-your-api-key-here" # 粘贴您的 DeepSeek API 密钥 +API_KEY = "sk-2292af2428d7419897ca1fb6e99ba6bc" # 替换为您的密钥 API_BASE_URL = "https://api.deepseek.com/v1" API_MODEL = "deepseek-chat" # 或 "deepseek-coder" API_TYPE = "deepseek" # ================================================== ``` -### 3. 可用模型 +**方法二:使用环境变量(推荐用于生产环境)** -| 模型名称 | 适用场景 | 特点 | -|---------|---------|------| -| `deepseek-chat` | 通用对话 | 平衡性能,推荐使用 | -| `deepseek-coder` | 代码相关 | 代码理解和生成能力强 | - -### 4. 费用说明 - -- 新用户通常有免费额度 -- 按 token 计费,价格实惠 -- 详见 [定价页面](https://platform.deepseek.com/pricing) - ---- - -## 🌐 Gemini API 配置 - -### 1. 获取 API 密钥 - -1. 访问 [Google AI Studio](https://aistudio.google.com/app/apikey) -2. 使用 Google 账号登录 -3. 点击"Get API Key" -4. 创建或选择项目 -5. 复制生成的 API 密钥 - -### 2. 配置到应用 - -编辑 `app.py` 文件的配置区域: - -```python -# ==================== API 配置 ==================== -API_KEY = "AIzaSy-your-gemini-api-key-here" -API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" -API_MODEL = "gemini-1.5-flash" # 或其他可用模型 -API_TYPE = "gemini" -# ================================================== -``` - -### 3. 可用模型 - -| 模型名称 | 特点 | -|---------|------| -| `gemini-1.5-flash` | 快速响应,适合实时交互 | -| `gemini-1.5-pro` | 更强大的理解和生成能力 | -| `gemini-pro` | 经典版本 | - -### 4. 注意事项 - -- Gemini API 在某些地区可能需要网络代理 -- 中国大陆用户推荐使用 DeepSeek API - ---- - -## 🔒 安全建议 - -### 方法 1:环境变量(推荐) - -不要直接在代码中硬编码 API 密钥,使用环境变量: - -**Windows PowerShell:** +1. **Windows PowerShell:** ```powershell -$env:DEEPSEEK_API_KEY="sk-your-key" +$env:DEEPSEEK_API_KEY="sk-2292af2428d7419897ca1fb6e99ba6bc" python app.py ``` -**Linux/Mac:** +2. **Linux / macOS:** ```bash -export DEEPSEEK_API_KEY="sk-your-key" +export DEEPSEEK_API_KEY="sk-2292af2428d7419897ca1fb6e99ba6bc" python app.py ``` -然后在代码中读取: +3. **在 config.py 中读取环境变量:** ```python import os + API_KEY = os.environ.get("DEEPSEEK_API_KEY", "") ``` -### 方法 2:配置文件 - -创建 `config.json`(不要提交到 Git): +**方法三:创建独立配置文件** +1. 在项目根目录创建 `config.json`: ```json { - "api_key": "sk-your-key", + "api_key": "sk-2292af2428d7419897ca1fb6e99ba6bc", "api_base_url": "https://api.deepseek.com/v1", "api_model": "deepseek-chat", "api_type": "deepseek" } ``` -在代码中加载: +2. 在 `config.py` 中加载: ```python import json +import os -with open('config.json', 'r') as f: - config = json.load(f) - API_KEY = config['api_key'] - API_BASE_URL = config['api_base_url'] - # ... +config_path = os.path.join(os.path.dirname(__file__), "config.json") +if os.path.exists(config_path): + with open(config_path, "r") as f: + config_data = json.load(f) + API_KEY = config_data.get("api_key", "") + API_BASE_URL = config_data.get("api_base_url", "https://api.deepseek.com/v1") + API_MODEL = config_data.get("api_model", "deepseek-chat") + API_TYPE = config_data.get("api_type", "deepseek") +else: + API_KEY = "" + API_BASE_URL = "https://api.deepseek.com/v1" + API_MODEL = "deepseek-chat" + API_TYPE = "deepseek" +``` + +### 2.4 DeepSeek 可用模型 + +| 模型名称 | 适用场景 | 特点 | 推荐场景 | +|----------|----------|------|----------| +| deepseek-chat | 通用对话 | 平衡性能与成本 | ⭐ 日常学习问答 | +| deepseek-coder | 代码相关 | 代码理解能力强 | 专业开发者 | + +### 2.5 费用说明 + +| 项目 | 说明 | +|------|------| +| 新用户优惠 | 通常有免费额度 | +| 计费方式 | 按 token 用量计费 | +| 价格水平 | 比 OpenAI 低约 80% | +| 查看用量 | https://platform.deepseek.com/usage | +| 详细定价 | https://platform.deepseek.com/pricing | + +### 2.6 速率限制 + +| 账户类型 | RPM(每分钟请求数) | TPM(每分钟 Token 数) | +|----------|---------------------|------------------------| +| 免费用户 | 60 | 100,000 | +| 付费用户 | 最高可达 2000 | 根据套餐 | + +--- + +## 三、Gemini API 配置 + +### 3.1 Gemini 简介 + +Gemini 是 Google 开发的 AI 大模型,具备: +- **gemini-1.5-flash**:快速响应,适合实时交互 +- **gemini-1.5-pro**:更强大的理解和生成能力 +- **多模态**:支持文本、图像等多种输入 + +**注意:** Gemini API 在中国大陆可能需要网络代理才能访问。 + +### 3.2 获取 API 密钥 + +**Step 1:访问 Google AI Studio** + +打开浏览器,访问:https://aistudio.google.com/app/apikey + +**Step 2:登录 Google 账号** + +使用您的 Google 账号登录。 + +**Step 3:获取 API 密钥** + +1. 点击 "Get API Key" +2. 选择或创建项目 +3. 点击 "Create API Key" +4. 复制生成的密钥 + +**密钥格式示例:** +``` +AIzaSy-your-gemini-api-key-here +``` + +### 3.3 配置到项目 + +编辑 `config.py` 文件: + +```python +# ==================== API 配置 ==================== +API_KEY = "AIzaSy-your-gemini-api-key-here" +API_BASE_URL = "https://generativelanguage.googleapis.com/v1beta" +API_MODEL = "gemini-1.5-flash" # 或 "gemini-1.5-pro" +API_TYPE = "gemini" +# ================================================== +``` + +### 3.4 Gemini 可用模型 + +| 模型名称 | 特点 | 适用场景 | 响应速度 | +|----------|------|----------|----------| +| gemini-1.5-flash | 快速响应 | 实时交互 | ⚡⚡⚡⚡⚡ | +| gemini-1.5-pro | 更强能力 | 复杂问题 | ⚡⚡⚡ | +| gemini-pro | 经典版本 | 一般对话 | ⚡⚡⚡⚡ | + +### 3.5 网络访问说明 + +| 地区 | 访问状态 | 解决方案 | +|------|----------|----------| +| 中国大陆 | 可能受限 | 使用 DeepSeek API 或配置代理 | +| 港澳台 | 基本正常 | 直连或使用代理 | +| 其他地区 | 正常 | 直连 | + +--- + +## 四、配置参数详解 + +### 4.1 完整配置参数表 + +| 参数名 | 类型 | 默认值 | 说明 | +|--------|------|--------|------| +| API_KEY | string | "" | API 密钥,必填 | +| API_BASE_URL | string | "https://api.deepseek.com/v1" | API 基础 URL | +| API_MODEL | string | "deepseek-chat" | 模型名称 | +| API_TYPE | string | "deepseek" | API 类型:deepseek 或 gemini | +| SERVER_NAME | string | "0.0.0.0" | 监听网络接口 | +| SERVER_PORT | int | 7860 | 监听端口 | +| SHARE | bool | True | 是否创建公开链接 | + +### 4.2 API_KEY 配置 + +**格式:** `sk-` 开头(DeepSeek)或 `AIzaSy` 开头(Gemini) + +**常见错误:** +- 密钥包含多余空格(复制时容易带入) +- 密钥过期或被删除 +- 密钥未激活对应服务 + +**排查方法:** +1. 确认密钥完整复制(无前后空格) +2. 在官网控制台确认密钥状态 +3. 确认密钥已绑定正确的产品/服务 + +### 4.3 API_BASE_URL 配置 + +| API 类型 | 正确 URL | 错误示例 | +|----------|----------|----------| +| DeepSeek | https://api.deepseek.com/v1 | https://api.deepseek.com/ | +| Gemini | https://generativelanguage.googleapis.com/v1beta | 其他 URL | + +### 4.4 API_MODEL 配置 + +**DeepSeek 模型:** + +| 模型 | 上下文长度 | 适用场景 | +|------|------------|----------| +| deepseek-chat | 64K tokens | 通用对话,推荐 | +| deepseek-coder | 64K tokens | 代码相关问题 | + +**Gemini 模型:** + +| 模型 | 上下文长度 | 适用场景 | +|------|------------|----------| +| gemini-1.5-flash | 1M tokens | 快速响应 | +| gemini-1.5-pro | 1M tokens | 复杂任务 | +| gemini-pro | 32K tokens | 一般对话 | + +--- + +## 五、流式响应配置 + +### 5.1 什么是流式响应 + +流式响应(Streaming)是指 AI 边生成答案边返回,用户可以实时看到回答内容,而不必等待完整答案生成完毕。 + +**优点:** +- 减少等待感 +- 及时了解回答方向 +- 支持长答案的快速预览 + +### 5.2 当前配置 + +本平台默认启用流式响应,配置位于 `chatbot.py`: + +```python +payload = { + "model": config.API_MODEL, + "messages": messages_for_api, + "stream": True, # 启用流式响应 + "temperature": 0.7, # 创造性参数 + "max_tokens": 2048 # 最大 Token 数 +} +``` + +### 5.3 参数调优 + +| 参数 | 取值范围 | 说明 | 调整建议 | +|------|----------|------|----------| +| temperature | 0.0 ~ 2.0 | 创造性控制,值越低越确定 | 学习问答建议 0.3~0.7 | +| max_tokens | 1 ~ 32768 | 单次回复最大 Token 数 | 长回答设为 4096 | +| top_p | 0.0 ~ 1.0 | 核采样参数 | 通常保持默认 1.0 | +| frequency_penalty | -2.0 ~ 2.0 | 频率惩罚 | 保持默认 0.0 | +| presence_penalty | -2.0 ~ 2.0 | 存在惩罚 | 保持默认 0.0 | + +--- + +## 六、系统提示词配置 + +### 6.1 系统提示词的作用 + +系统提示词(System Prompt)定义了 AI 助手的角色定位、回答风格和专业范围。本平台的默认提示词位于 `chatbot.py`。 + +### 6.2 默认提示词 + +```python +system_prompt = """你是一位精通自动控制原理的专家教授。请用清晰、准确、专业的中文来回答有关自动控制课程内容的问题。 + +重要规则: +1. 当需要表达数学公式时,必须使用 LaTeX 格式 +2. 行内公式使用 $公式$ 或 \\(公式\\) +3. 独立公式使用 $$公式$$ 或 \\[公式\\] +4. 例如:传递函数可以写成 $G(s) = \\frac{K}{s(s+1)}$ +5. 二阶系统标准形式:$$G(s) = \\frac{\\omega_n^2}{s^2 + 2\\zeta\\omega_n s + \\omega_n^2}$$ + +请在适当的时候使用公式和示例来辅助解释。""" +``` + +### 6.3 自定义提示词 + +根据教学需求,您可以修改系统提示词: + +**修改方法:** 编辑 `chatbot.py` 中的 `system_prompt` 变量。 + +**示例1:强化公式推导** +```python +system_prompt = """你是一位严谨的自动控制原理教授。在回答问题时: +1. 注重公式的推导过程 +2. 每一步推导都要清晰呈现 +3. 适当使用 LaTeX 公式 +4. 结合实例帮助理解""" +``` + +**示例2:简化回答风格** +```python +system_prompt = """你是一位friendly的自动控制课程助教。请用简洁、易懂的语言回答问题: +1. 尽量少用专业术语 +2. 多用生活实例类比 +3. 重要公式用 LaTeX 展示""" +``` + +**示例3:英文问答模式** +```python +system_prompt = """You are an expert professor of Automatic Control Theory. +Answer questions in English using LaTeX for mathematical formulas. +Focus on clarity and practical examples.""" ``` --- -## ❓ 常见问题 +## 七、安全建议 -### Q1: API 请求失败,显示 401 错误 +### 7.1 密钥安全原则 -**原因**:API 密钥无效或未配置 +**❌ 不要做的事情:** +- 在代码中硬编码密钥并提交到 Git +- 在公开场合分享密钥 +- 使用过于简单的密钥 -**解决**: -1. 检查 API 密钥是否正确复制(无多余空格) -2. 确认密钥未过期或被删除 -3. 重新生成密钥并更新配置 +**✅ 推荐的做法:** +- 使用环境变量存储密钥 +- 将敏感配置文件加入 .gitignore +- 定期更换密钥 +- 为不同项目使用不同的密钥 -### Q2: 网络连接错误 +### 7.2 .gitignore 配置 -**原因**:网络问题或 API 服务不可达 +确保以下文件不会被提交到 Git: -**解决**: -1. DeepSeek 用户:检查国内网络连接 -2. Gemini 用户:可能需要配置网络代理 -3. 尝试切换到 DeepSeek API(国内友好) +``` +# API 配置文件 +config.json +secrets.json +.env -### Q3: 回复速度慢或超时 +# Python +__pycache__/ +*.pyc +*.pyo -**原因**:网络延迟或 API 负载高 +# IDE +.vscode/ +.idea/ -**解决**: -1. 检查网络连接速度 -2. 调整超时设置(app.py 中的 `ClientTimeout`) -3. 尝试切换模型(如 flash 版本) +# 模型权重(较大文件) +Model/data/*.pth +``` -### Q4: 公式不渲染 +### 7.3 生产环境部署 -**原因**:Chatbot 未启用 LaTeX 支持 +**推荐做法:** -**解决**: -确认 `gr.Chatbot` 包含 `latex_delimiters` 参数: +1. **使用环境变量** +```bash +# Docker 部署 +docker run -p 7860:7860 \ + -e DEEPSEEK_API_KEY="sk-xxx" \ + autocontrol-course +``` + +2. **使用配置服务** +- AWS Secrets Manager +- Azure Key Vault +- HashiCorp Vault + +3. **限制 API 访问** +- 设置 API 密钥的使用 IP 白名单 +- 配置请求频率限制 +- 开启使用量告警 + +--- + +## 八、常见问题排查 + +### 8.1 API 请求失败 + +**问题1:401 Unauthorized** + +| 可能原因 | 解决方法 | +|----------|----------| +| API 密钥无效 | 检查密钥是否正确复制 | +| 密钥已过期 | 在平台控制台重新创建密钥 | +| 密钥未激活 | 确认密钥已绑定正确服务 | + +**问题2:403 Forbidden** + +| 可能原因 | 解决方法 | +|----------|----------| +| 账户余额不足 | 充值或等待免费额度刷新 | +| 权限不足 | 检查账户权限设置 | +| 服务未开通 | 在控制台开通对应服务 | + +**问题3:429 Rate Limit** + +| 可能原因 | 解决方法 | +|----------|----------| +| 请求过于频繁 | 降低请求频率 | +| 超出 TPM/RPM 限制 | 等待或升级套餐 | +| 并发数过高 | 减少并发请求数 | + +**解决方法:** +1. 等待一段时间后重试 +2. 配置请求间隔(如每次提问间隔 2 秒) +3. 升级到更高配额套餐 + +### 8.2 网络连接问题 + +**问题:Connection Error / Timeout** + +**排查步骤:** + +1. **检查网络连接** +```bash +# 测试 API 端点是否可达 +curl -I https://api.deepseek.com/v1 +``` + +2. **检查代理设置(如需要)** +```python +# 在 chatbot.py 中配置代理 +import os +os.environ["HTTP_PROXY"] = "http://proxy.example.com:8080" +os.environ["HTTPS_PROXY"] = "http://proxy.example.com:8080" +``` + +3. **增加超时时间** +```python +# 在 aiohttp 请求中增加 timeout +async with session.post(api_url, json=payload, headers=headers, + timeout=aiohttp.ClientTimeout(total=120)) as response: +``` + +### 8.3 回复质量问题 + +**问题:回复内容不准确** + +**解决方法:** + +1. **优化系统提示词** + - 明确指定回答风格 + - 强调专业领域要求 + - 添加示例回答 + +2. **调整 temperature 参数** + - 降低 temperature(0.3~0.5)使回答更确定 + - 提高 temperature(0.7~1.0)使回答更有创造性 + +3. **优化提问方式** + - 提供更多上下文 + - 明确问题范围 + - 指出具体困惑点 + +**问题:回复速度慢** + +**解决方法:** + +1. **使用较轻量的模型** + - DeepSeek:选择 deepseek-chat 而非 deepseek-coder + - Gemini:选择 gemini-1.5-flash + +2. **减少 max_tokens** + - 根据实际需求设置合理的最大长度 + - 避免生成过长的回答 + +3. **检查网络延迟** + - 选择距离更近的 API 端点 + - 考虑使用 CDN 加速 + +### 8.4 公式渲染问题 + +**问题:LaTeX 公式不显示** + +**可能原因:** + +1. **Chatbot 未启用 LaTeX** +2. **MathJax 加载失败** +3. **公式语法错误** + +**解决方法:** + +1. 确认 `gr.Chatbot` 配置包含 `latex_delimiters`: ```python chatbot = gr.Chatbot( latex_delimiters=[ {"left": "$$", "right": "$$", "display": True}, - {"left": "$", "right": "$", "display": False} + {"left": "$", "right": "$", "display": False}, + {"left": "\\[", "right": "\\]", "display": True}, + {"left": "\\(", "right": "\\)", "display": False} ] ) ``` -### Q5: 如何限制 API 调用成本? +2. 刷新页面重试 -**建议**: -1. 在 API 平台设置使用限额 -2. 代码中添加 `max_tokens` 限制 -3. 监控 API 使用情况 -4. 使用轻量级模型(如 flash 版本) +3. 检查 LaTeX 语法是否正确 --- -## 📞 获取帮助 +## 九、API 使用成本优化 -- **DeepSeek 文档**:https://platform.deepseek.com/docs -- **Gemini 文档**:https://ai.google.dev/docs -- **项目 Issues**:[GitHub Issues 链接] +### 9.1 成本构成 + +API 使用成本主要由以下因素决定: + +| 因素 | 说明 | 优化建议 | +|------|------|----------| +| 输入 Token 数 | 问题文本长度 | 精简提问 | +| 输出 Token 数 | 回答文本长度 | 限制 max_tokens | +| 请求次数 | 提问频率 | 减少无效请求 | +| 模型单价 | 不同模型价格不同 | 选择性价比模型 | + +### 9.2 优化策略 + +**策略1:精简提问** +- 移除问题中不必要的修饰词 +- 明确指出核心疑问 +- 提供必要的上下文但不过度 + +**策略2:合理限制输出长度** +- 根据问题类型设置 max_tokens +- 简单问题设置较短限制 +- 复杂问题允许更长回答 + +**策略3:缓存常用回答** +- 实现本地缓存机制 +- 避免重复提问相同问题 +- 减少 API 调用次数 + +**策略4:选择合适模型** +- 日常问答:使用轻量模型(flash 版本) +- 复杂问题:按需使用强大模型 + +### 9.3 预算设置 + +在 DeepSeek 控制台设置用量限制: + +1. 访问 https://platform.deepseek.com/ +2. 进入 "用量限制" 设置 +3. 设置月度预算上限 +4. 开启用量告警 --- -最后更新:2025年10月15日 +## 十、获取帮助 + +### 10.1 官方文档 + +| 资源 | 链接 | +|------|------| +| DeepSeek 文档 | https://platform.deepseek.com/docs | +| Gemini 文档 | https://ai.google.dev/docs | +| Gradio 文档 | https://gradio.app/docs | + +### 10.2 技术支持 + +| 渠道 | 联系方式 | +|------|----------| +| 项目问题 | GitHub Issues | +| API 问题 | 平台官方支持 | +| 使用咨询 | 课程教师/助教 | + +--- + +**最后更新:2026年4月7日** diff --git a/CHANGELOG.md b/CHANGELOG.md index aa38048..2e4856d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,76 +1,379 @@ -# Changelog +# 更新日志 (Changelog) -All notable changes to this project will be documented in this file. +> 本文档记录自动控制理论AI+数智平台的所有重要更新,包括新增功能、功能改进、问题修复与技术变更。 -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +本文档格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/) 规范,并遵循 [语义化版本 (SemVer)](https://semver.org/lang/zh-CN/) 约定。 + +--- + +## 版本命名规范 + +版本号格式:`主版本.次版本.修订号` + +| 标识 | 含义 | +|------|------| +| 主版本 (MAJOR) | 不兼容的重大架构变更 | +| 次版本 (MINOR) | 向后兼容的新功能添加 | +| 修订号 (PATCH) | 向后兼容的问题修复 | + +--- ## [1.2.0] - 2026-04-07 -### Added -- ✨ 新增“算例演示(Case Demo)”标签页,位于根轨迹与智能问答之间 -- ✨ 新增 `case_demo_functions.py`,支持参数化工况运行完整混动模型 -- ✨ 新增图文输出:转速响应、功率分配、电气状态、关键时刻数据表 -- ✨ 新增算例结果自动解读(最大转速误差、SOC变化、平均功率、平均燃油流量) +> **重要更新:新增算例演示模块与完整混动模型联动** -### Changed -- 🔧 `Model/src/series_hybrid_sim.py` 改为按 Model 目录定位 GPR 数据与权重 -- 🔧 `requirements.txt` 固定关键依赖版本并加入 PyTorch CPU 下载源 -- 🔧 文档更新:README 与 GUIDANCE 同步为“五大功能”结构与最新运行方式 +### Added(新增功能) -### Removed -- 🧹 精简 `Model` 目录:移除 `scripts/`、`EngineData.xlsx`、`.git/`、`.claude/`、`README.md`、`LICENSE`、`environment.yml`、`figures/`、`.gitignore` +#### 🧪 算例演示模块(Case Demo) + +本版本最核心的更新是新增了完整的算例演示模块,通过串联式混合动力系统模型展示控制系统设计在实际工程中的应用。 + +**阶段零:模型训练** +- 新增 `case_demo_functions.py` 模块,作为算例演示的核心入口 +- 新增 GPR(高斯过程回归)模型训练/加载功能 + - 支持从头训练(需要 botorch/gpytorch/sklearn) + - 支持加载已有 .pth 权重文件 + - 生成训练/验证结果可视化(散点图、方差热力图) +- 新增 NN 模型训练(知识蒸馏)功能 + - 将 GPR 教师模型的知识蒸馏到轻量级 MLP + - 可视化训练损失曲线、燃油流量与功率的 parity plot + - 支持自定义 epochs、learning_rate、hidden_size 参数 + +**阶段一:发动机控制器设计** +- 新增基于 NN 代理模型的涡轴发动机动态仿真 +- 新增 PID 控制器(增量式) + - 可调参数:Kp、Ki、Kd + - 支持输入/输出量程归一化 +- 新增 MPC 控制器(模型预测控制) + - 纯 Python 实现的投影梯度下降求解器 + - 可调参数:预测时域、功率跟踪权重、控制增量权重、超调限制 + - 支持超调量硬约束(5%) + +**阶段二:电机控制器设计** +- 新增永磁同步电机(PMSM)离散时间动力学模型 +- 新增 d/q 轴电流控制与 MTPA 控制策略 +- 新增 PID / MPC 两种控制器 +- 新增负载扰动测试(60% 时刻施加 150% 额定负载) + +**阶段三:能量管理策略设计** +- 新增基于规则的功率跟随策略 +- 新增 SOC 滞环控制(防止模式频繁切换) +- 新增完整混动系统仿真(发动机 + 电机 + 电池) +- 新增三联图输出(转速响应/功率分配/电气状态) +- 新增关键时刻数据表(8 个关键时刻点) + +**模型核心模块(Model/src/)** + +| 文件 | 功能 | 关键特性 | +|------|------|----------| +| `lightweight_model.py` | NN 代理模型 | MLP 3→64→64→2,Tanh 激活,归一化处理 | +| `engine_gpr_class.py` | GPR 模型 | 高斯过程回归,支持批量预测 | +| `distill_gpr_to_nn.py` | 知识蒸馏脚本 | CSV 直接训练或 GPR 蒸馏 | +| `engine_dynamic_sim.py` | 涡轴发动机仿真 | 燃油执行机构 + 转子动力学 | +| `motor_sim.py` | PMSM 电机仿真 | SVPWM、Clarke/Park 变换、损耗模型 | +| `battery_sim.py` | 电池仿真 | OCV-SOC 查表、内阻模型、安时积分 | +| `mpc_controller.py` | MPC 控制器 | 投影梯度下降、硬约束处理 | +| `increPID.py` | 增量式 PID | 归一化缩放、自动抗积分饱和 | +| `series_hybrid_sim.py` | 混动系统总成 | 功率平衡、滞环控制 | + +### Changed(功能变更) + +#### 📁 Model 目录结构优化 + +为提高项目可维护性,对 Model 目录进行了精简: + +**移除的文件/目录:** +- `scripts/` 目录(功能已整合到 `case_demo_functions.py`) +- `EngineData.xlsx`(原始数据,已被 CSV 替代) +- `.git/`(子模块 Git 历史) +- `.claude/`(IDE 配置) +- `README.md`(已整合到主项目文档) +- `LICENSE`(已使用主项目许可证) +- `environment.yml`(conda 环境配置) +- `figures/`(输出目录) +- `.gitignore`(已使用主项目配置) + +**保留的核心文件:** +- `Model/src/` — 所有源代码 +- `Model/data/` — 模型权重与数据文件 +- `requirements.txt` — 依赖列表 + +#### 模型路径解析改进 + +`series_hybrid_sim.py` 等文件改为按 Model 目录定位资源: + +```python +# 旧方式(可能因工作目录而失败) +nn_pth = "Model/data/engine_nn_proxy.pth" + +# 新方式(基于脚本位置定位) +MODEL_DATA_PATH = os.path.join(os.path.dirname(__file__), "..", "data") +nn_pth = os.path.join(MODEL_DATA_PATH, "engine_nn_proxy.pth") +``` + +#### 依赖版本锁定 + +`requirements.txt` 更新如下: + +| 依赖 | 新版本 | 变更说明 | +|------|--------|----------| +| gradio | 4.44.1 | 升级到最新稳定版 | +| gradio-client | 1.3.0 | 新增 | +| pydantic | 2.10.6 | 升级 | +| pydantic-core | 2.27.2 | 升级 | +| huggingface_hub | 0.23.0 | 新增 | +| numpy | 1.26.4 | 锁定版本 | +| control | 0.9.4 | 锁定版本 | +| matplotlib | 3.9.4 | 升级 | +| aiohttp | 3.13.5 | 升级 | +| pillow | 10.4.0 | 升级 | +| torch | 2.4.1+cu121 | 新增(GPU 版本) | +| botorch | 0.14.0 | 新增(GPR 依赖) | +| gpytorch | 1.14 | 新增(GPR 依赖) | +| pyro-ppl | 1.9.1 | 新增(GPR 依赖) | +| pandas | 2.3.3 | 新增(数据处理) | +| scipy | >=1.10 | 新增(数值计算) | +| scikit-learn | 1.7.1 | 新增(数据归一化) | +| psutil | >=5.9 | 新增(系统监控) | + +**PyTorch 下载源配置:** +```txt +--extra-index-url https://download.pytorch.org/whl/cu121 +``` + +#### 文档同步更新 + +- `README.md`:同步为"五大功能"结构,新增算例演示详解 +- `GUIDANCE.md`:新增四阶段操作指南、混动系统架构说明 +- `API_CONFIG.md`:新增 API 配置详解、安全建议 +- `CHANGELOG.md`:新增详细版本记录 + +### Removed(移除功能) + +| 移除项 | 原位置 | 替代方案 | +|--------|--------|----------| +| 独立训练脚本 | `Model/scripts/distill_gpr_to_nn.py` | 已整合到 `case_demo_functions.py` | +| Excel 数据文件 | `Model/EngineData.xlsx` | 使用 `Model/data/Cleaned_Engine_Data_Full.csv` | +| conda 环境文件 | `Model/environment.yml` | 使用 `requirements.txt` | + +### Fixed(问题修复) + +| 问题 | 修复内容 | +|------|----------| +| Model 路径解析错误 | 改用 `__file__` 相对定位,避免工作目录影响 | +| GPR 训练缺少依赖提示 | 新增清晰的依赖安装指引 | +| 模型权重未找到 | 提供更详细的错误提示与解决步骤 | + +--- + +## [1.1.0] - 2026-02-15 + +> **功能增强与问题修复** + +### Added(新增功能) + +| 功能 | 说明 | +|------|------| +| 实时在线人数统计 | 页面顶部显示当前在线用户数与历史总人数 | +| 总访问人数统计 | 持久化记录累计访问人数 | +| 系统资源监控 | 显示 CPU、内存、GPU 使用率 | +| 页面自动刷新 | 每 10 秒更新在线人数,每 3 秒更新系统资源 | + +### Changed(功能变更) + +| 变更项 | 变更内容 | +|--------|----------| +| 在线统计存储 | 从内存改为 JSON 文件持久化 | +| 统计更新频率 | 在线人数每 10 秒刷新,资源每 3 秒刷新 | +| 界面布局优化 | 顶部状态栏与系统监控整合 | + +### Fixed(问题修复) + +| 问题 | 修复内容 | +|------|----------| +| 页面刷新导致在线人数重置 | 改用 Session ID 跟踪用户会话 | +| 多用户并发统计不准 | 添加线程锁保护共享状态 | + +--- ## [1.0.0] - 2025-10-15 -### Added -- ✨ 时域分析功能(阶跃响应、脉冲响应、性能指标计算) -- ✨ 频域分析功能(Bode 图、Nyquist 图、稳定裕度) -- ✨ 根轨迹分析功能(动态轨迹绘制、增益调节、极点跟踪) -- ✨ AI 智能问答功能(支持 DeepSeek 和 Gemini API) -- 🎨 现代化 UI 设计(渐变色、卡片布局、可滚动知识区) -- 📚 详细的知识卡片(时域、频域、根轨迹理论) -- 🔧 对数增益滑块(精确调节 0.1 到 1000 范围) -- 💬 LaTeX 公式渲染(聊天机器人内数学公式支持) -- 📊 英文图表标签(避免中文显示问题) +> **首次正式发布** -### Features -- 支持任意阶次线性时不变(LTI)系统分析 -- 实时参数调节和图表更新 -- 流式 AI 对话响应 -- 标签页切换自动加载数据 -- 可折叠的知识点章节 +### Added(新增功能) -### Documentation -- 📄 完整的 README.md -- 📄 API 配置指南(API_CONFIG.md) -- 📄 快速上手指南(QUICK_START.md) -- 📄 依赖列表(requirements.txt) -- 📄 .gitignore 配置 -- 📄 MIT 开源许可证 +#### 🎯 核心分析功能 -## [Unreleased] +**1. 时域分析模块** +- 单位阶跃响应分析与绘图 +- 单位脉冲响应分析与绘图 +- 自动性能指标计算: + - 上升时间 (Rise Time) + - 峰值时间 (Peak Time) + - 超调量 (Overshoot) + - 调节时间 (Settling Time) + - 稳态值 (Steady State Value) +- 传递函数系数解析与 LaTeX 渲染 -### Planned -- [ ] 状态空间分析模块 -- [ ] 离散系统分析支持 -- [ ] 更多控制器设计工具(PID 调优、极点配置) -- [ ] 系统对比功能(多个传递函数对比) -- [ ] 导出分析报告(PDF/Word) -- [ ] 历史记录保存 -- [ ] 更多 AI 模型支持 -- [ ] 多语言界面(英文版) -- [ ] 移动端适配 +**2. 频域分析模块** +- Bode 图绘制(幅频特性 + 相频特性) +- Nyquist 图绘制(极坐标频率响应) +- 自动增益裕度 (GM) 计算 +- 自动相位裕度 (PM) 计算 +- 稳定性自动判断 + +**3. 根轨迹分析模块** +- 完整根轨迹自动绘制 +- 对数增益滑块(log₁₀(K) 范围 -4 到 4) +- 实时极点位置显示 +- 动态坐标范围调整 +- 阻尼比等值线参考 + +**4. AI 智能问答模块** +- DeepSeek API 集成 +- Gemini API 备用支持 +- 流式响应(实时显示生成过程) +- 多轮对话上下文记忆 +- LaTeX 数学公式渲染 +- 自动控制原理专业问答 + +#### 🎨 界面设计 + +| 特性 | 说明 | +|------|------| +| 渐变色标题 | 紫色渐变视觉效果 | +| 卡片式布局 | 分组清晰,层次分明 | +| 可滚动知识卡片 | 节省屏幕空间 | +| 可折叠章节 | 按需展开 | +| 实时参数更新 | 图表即时反映变化 | +| 标签页联动 | 切换自动加载数据 | +| 平滑动画效果 | 悬停、滚动动效 | + +#### 📚 知识卡片内容 + +| 模块 | 知识点 | +|------|--------| +| 时域分析 | 二阶系统标准形式、阻尼比与响应特性、性能指标公式、稳态误差分析 | +| 频域分析 | Bode 图绘制技巧、稳定裕度定义、稳定性判断准则、频域-时域对应关系 | +| 根轨迹 | 基本规则、起点终点、渐近线、分离点、s 平面稳定性区域 | + +#### 🔧 交互设计 + +| 特性 | 实现 | +|------|------| +| 对数增益滑块 | log₁₀(K) 范围 -4~4,对应 K = 0.0001~10000 | +| 传递函数输入 | 系数从高次幂到常数项,逗号分隔 | +| 实时预览 | 显示传递函数 LaTeX 公式 | +| 松开更新 | 滑块释放后才更新图表,避免卡顿 | + +### Documentation(文档) + +| 文档 | 内容 | +|------|------| +| README.md | 项目简介、功能说明、快速开始、技术栈 | +| GUIDANCE.md | 学生使用指南、详细教程、示例库 | +| API_CONFIG.md | DeepSeek/Gemini API 配置指南 | +| CHANGELOG.md | 版本更新记录 | +| requirements.txt | Python 依赖列表 | +| .gitignore | Git 忽略配置 | + +### Features(技术特性) + +| 特性 | 说明 | +|------|------| +| 任意阶次 LTI 系统 | 支持任意阶次线性时不变系统分析 | +| python-control 集成 | 复用成熟控制系统工具箱 | +| 英文图表标签 | 避免中文显示问题 | +| LaTeX 公式渲染 | Chatbot 内数学公式支持 | --- -## Version History +## [Unreleased] - 开发中 -### v1.0.0 (2025-10-15) -- 🎉 首次正式发布 -- 包含四大核心功能模块 -- 完整的文档和配置文件 +> 以下为计划中但尚未发布的功能 + +### Planned(计划功能) + +| 功能 | 状态 | 说明 | +|------|------|------| +| 状态空间分析模块 | 🔄 计划中 | 状态空间模型构建、能控性/能观性分析 | +| 离散系统分析支持 | 🔄 计划中 | 离散传递函数、Z变换、根轨迹 | +| PID 控制器自动调参 | 🔄 计划中 | Ziegler-Nichols、遗传算法优化 | +| 极点配置设计工具 | 🔄 计划中 | 通过状态反馈实现指定极点位置 | +| 系统对比功能 | 🔄 计划中 | 多个传递函数对比分析 | +| 分析报告导出 | 🔄 计划中 | PDF/Word 格式报告生成 | +| 历史记录保存 | 🔄 计划中 | 本地保存分析历史 | +| 更多 AI 模型支持 | 🔄 计划中 | Claude、GPT-4 等 | +| 多语言界面 | 🔄 计划中 | 英文版界面 | +| 移动端适配 | 🔄 计划中 | 响应式布局优化 | --- -**Note**: For detailed commit history, see the [Git log](https://github.com/your-repo/commits). +## 版本历史 + +| 版本 | 日期 | 重大变更 | +|------|------|----------| +| 1.2.0 | 2026-04-07 | 新增算例演示模块、混动模型联动、精简 Model 目录 | +| 1.1.0 | 2026-02-15 | 新增在线统计、系统资源监控、持久化存储 | +| 1.0.0 | 2025-10-15 | 首次正式发布,四大核心功能 + AI 问答 | + +--- + +## 分支管理 + +| 分支 | 用途 | +|------|------| +| `master` | 主分支,稳定版本 | +| `feature/case-demo-model-minimal` | 算例演示模块开发分支 | + +### 合并策略 + +```bash +# 功能完成后,创建 Pull Request +git checkout -b feature/your-feature +git add . +git commit -m "feat: add new feature" +git push -u origin feature/your-feature + +# 合并到 master +git checkout master +git merge feature/your-feature +git push origin master +``` + +--- + +## 贡献者 + +| 贡献者 | 角色 | 主要贡献 | +|--------|------|----------| +| 魏鹏飞 | 项目负责人 | 整体架构设计、核心算法 | +| 项目团队 | 开发 | 各功能模块实现 | + +--- + +## 提交信息规范 + +本项目采用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: + +| 类型 | 说明 | +|------|------| +| `feat:` | 新功能 | +| `fix:` | 问题修复 | +| `docs:` | 文档更新 | +| `style:` | 代码格式调整(不影响功能) | +| `refactor:` | 代码重构 | +| `perf:` | 性能优化 | +| `test:` | 测试相关 | +| `chore:` | 构建/工具变更 | + +**示例:** +```bash +git commit -m "feat: add MPC controller for engine design" +git commit -m "fix: resolve path resolution issue in Model loading" +git commit -m "docs: update README with new case demo section" +``` + +--- + +**注意:** 详细的 Git 提交历史请参阅 `git log` 或 GitHub 仓库的 Commit 页面。 diff --git a/GUIDANCE.md b/GUIDANCE.md index f83ccd4..785aaab 100644 --- a/GUIDANCE.md +++ b/GUIDANCE.md @@ -1,311 +1,851 @@ -# 自动控制理论AI+数智平台 - 学生使用指南 +# 自动控制理论AI+数智平台 — 学生使用指南 -## 一、平台访问 - -**平台地址**: http://182.148.54.29:7860/ - -**推荐浏览器**: Chrome、Edge 或 Firefox 最新版本 +> 本指南面向使用本平台进行自动控制理论课程学习的本科生,详细介绍平台功能、使用方法、学习建议与常见问题解决方案。 --- -## 二、平台功能简介 +## 一、平台概述 -本平台是《自动控制理论》课程的配套学习工具,提供五大核心功能: +### 1.1 平台简介 -1. **时域分析** - 分析系统的阶跃响应和脉冲响应 -2. **频域分析** - 绘制Bode图和Nyquist图,分析系统稳定性 -3. **根轨迹分析** - 观察增益变化对系统极点的影响 -4. **算例演示** - 基于完整混动发动机模型的参数化仿真 -5. **AI智能问答** - 24小时在线的自动控制理论助教 +自动控制理论AI+数智平台是西北工业大学2025年校级本科生建设项目的核心成果,旨在为自动控制理论课程提供交互式、数智化的学习环境。平台集成了系统分析工具、完整混动模型算例演示与AI智能问答功能,覆盖从经典控制理论到现代控制工程的多个核心知识点。 + +### 1.2 访问方式 + +**在线访问(本校学生优先):** +``` +http://182.148.54.29:7860/ +``` + +**本地运行(需配置Python环境):** +```bash +# 克隆项目后 +pip install -r requirements.txt +python app.py +# 访问 http://localhost:7860 +``` + +### 1.3 支持浏览器 + +| 浏览器 | 推荐版本 | 备注 | +|--------|----------|------| +| Google Chrome | 90+ | 首选,推荐使用 | +| Microsoft Edge | 90+ | 完全支持 | +| Mozilla Firefox | 88+ | 完全支持 | +| Apple Safari | 14+ | 基本支持 | + +### 1.4 界面布局 + +平台主界面分为以下区域: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 🌐 在线人数:12 👥 总人数:348 (顶部状态栏) │ +├─────────────────────────────────────────────────────────────┤ +│ 🖥️ CPU 45% 💾 RAM 8.2/16 GB 🎮 GPU N/A │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ⏱️ 时域分析 │ 📊 频域分析 │ 🎯 根轨迹 │ 🧪 算例演示 │ 🤖 智能问答 │ +│ (Tab 0) │ (Tab 1) │ (Tab 2) │ (Tab 3) │ (Tab 4) │ +│ │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ [左侧控制面板] [中间图表区域] │ +│ - 参数输入框 - 时域响应曲线 │ +│ - 增益滑块 - Bode图/Nyquist图 │ +│ - 性能指标显示 - 根轨迹图 │ +│ - 混动仿真结果 │ +│ │ +│ [右侧知识卡片] │ +│ - 理论知识点 │ +│ - 公式与推导 │ +│ - 使用技巧 │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` --- -## 三、详细使用教程 +## 二、功能模块详解 -### 3.1 时域分析 +### 2.1 时域分析 -#### 3.1.1 功能说明 +#### 2.1.1 功能说明 -输入系统的传递函数,查看系统的时域响应特性。 +时域分析模块用于研究控制系统对时间域输入信号的响应特性。通过输入系统的传递函数,可以观察单位阶跃响应和单位脉冲响应,并自动计算上升时间、峰值时间、超调量、调节时间、稳态值等性能指标。 -#### 3.1.2 使用步骤 +#### 2.1.2 传递函数输入格式 -1. 点击顶部"时域分析"标签页 -2. 在左侧输入框中输入: - - **分子系数**:例如 `1` 或 `1,2,3`(用逗号分隔) - - **分母系数**:例如 `1,6,11,6`(从最高次项到常数项) -3. 点击"显示传递函数"按钮查看数学公式 -4. 点击"开始分析"按钮生成响应曲线 +传递函数的标准形式为: -#### 3.1.3 输出结果 +$$G(s) = \frac{b_0 s^m + b_1 s^{m-1} + \cdots + b_m}{a_0 s^n + a_1 s^{n-1} + \cdots + a_n}$$ -- **左图**:单位阶跃响应曲线 -- **右图**:单位脉冲响应曲线 -- **性能指标**:上升时间、峰值时间、超调量、调节时间等 +**输入方法:** -#### 3.1.4 示例输入 +1. 在「分子系数」输入框中填写分子多项式系数(从最高次幂到常数项) +2. 在「分母系数」输入框中填写分母多项式系数(从最高次幂到常数项) +3. 系数之间用**英文逗号**分隔 -```text +**示例:** + +| 系统类型 | 示例 | 分子输入 | 分母输入 | +|----------|------|----------|----------| +| 一阶系统 | $G(s) = \frac{1}{s+1}$ | `1` | `1,1` | +| 二阶系统 | $G(s) = \frac{4}{s^2+2s+4}$ | `4` | `1,2,4` | +| 二阶系统(重根)| $G(s) = \frac{1}{s^2+2s+1}$ | `1` | `1,2,1` | +| 三阶系统 | $G(s) = \frac{1}{s^3+6s^2+11s+6}$ | `1` | `1,6,11,6` | +| 有零点系统 | $G(s) = \frac{s+2}{s^2+3s+2}$ | `1,2` | `1,3,2` | + +#### 2.1.3 操作步骤 + +1. **切换到「⏱️ 时域分析」标签页** +2. **输入传递函数系数** + - 点击「✓ 显示传递函数」按钮预览传递函数公式 + - 确认公式显示正确后再继续 +3. **点击「🚀 开始分析」按钮** +4. **查看分析结果** + - 左图:单位阶跃响应曲线 + - 右图:单位脉冲响应曲线 + - 下方:性能指标数值 + +#### 2.1.4 性能指标解读 + +| 指标名称 | 含义 | 合格标准(典型值) | +|----------|------|-------------------| +| 上升时间 (Rise Time) | 响应从10%上升到90%稳态值的时间 | 越小越快 | +| 峰值时间 (Peak Time) | 响应达到第一个峰值的时间 | 越小越快 | +| 超调量 (Overshoot) | 峰值超出稳态值的百分比 | < 20% 为宜 | +| 调节时间 (Settling Time) | 响应进入±2%误差带并保持的时间 | 越小越好 | +| 稳态值 (Steady State) | 时间趋于无穷时的系统输出 | 应等于1(单位阶跃) | + +#### 2.1.5 学习建议 + +**练习方向:** +1. 对比不同阻尼比下二阶系统的响应特性 +2. 观察零点位置对系统响应的影响 +3. 分析系统增益变化对稳态误差的影响 + +**推荐传递函数参数:** + +| 阻尼比 | 分子 | 分母 | 响应特点 | +|--------|------|------|----------| +| 无阻尼 | `4` | `1,0,4` | 等幅振荡 | +| 欠阻尼 | `1` | `1,0.6,1` | 振荡衰减 | +| 临界阻尼 | `1` | `1,2,1` | 最快无振荡 | +| 过阻尼 | `1` | `1,3,1` | 无振荡,较慢 | + +--- + +### 2.2 频域分析 + +#### 2.2.1 功能说明 + +频域分析模块通过绘制Bode图和Nyquist图来分析系统的频率响应特性,并计算增益裕度和相位裕度这两个重要的稳定性指标。 + +#### 2.2.2 操作步骤 + +1. **切换到「📊 频域分析」标签页** +2. **输入传递函数系数**(与时域分析相同) +3. **调整增益** + - 拖动「对数增益 log₁₀(K)」滑块 + - 或在数字框中直接输入K值 +4. **观察实时更新的图表** + +#### 2.2.3 图表解读 + +**Bode图(左上+左下):** +- **上半图**:幅频特性,Y轴为增益(dB),X轴为频率(rad/s) +- **下半图**:相频特性,Y轴为相位(度),X轴为频率(rad/s) +- **红色虚线**:$-180°$ 相位线(用于判断相位裕度) + +**Nyquist图(右侧):** +- 极坐标形式显示频率响应 +- 曲线上的标记点代表不同频率 +- 原点为 $(0, j0)$,实轴正方向为 $0°$ + +#### 2.2.4 稳定裕度 + +| 指标 | 定义 | 稳定条件 | 经验要求 | +|------|------|----------|----------| +| 增益裕度 (GM) | 相角为 $-180°$ 时增益还能增大的倍数 | $GM_{dB} > 0$ | $> 6$ dB | +| 相位裕度 (PM) | 增益为0dB时相位还能滞后的度数 | $PM > 0°$ | $> 45°$ | + +**稳定性判断:** +- `GM > 0 dB` 且 `PM > 0°` → 🟢 **系统稳定** +- `GM < 0 dB` 或 `PM < 0°` → 🔴 **系统不稳定** + +#### 2.2.5 频域与时域的关系 + +| 频域指标 | 时域对应 | 经验公式 | +|----------|----------|----------| +| 带宽 $\omega_b$ | 上升时间 $t_r$ | $t_r \approx 1.8/\omega_b$ | +| 相位裕度 PM | 超调量 $\sigma\%$ | $\sigma \approx 100 \times e^{-\pi PM/(90-PM)}$ | + +#### 2.2.6 学习建议 + +**练习方向:** +1. 找出系统的增益交越频率和相位交越频率 +2. 观察增益K变化对稳定裕度的影响 +3. 理解Bode图与Nyquist图的对应关系 + +--- + +### 2.3 根轨迹分析 + +#### 2.3.1 功能说明 + +根轨迹法是一种图解方法,通过观察闭环极点(系统稳定性由极点位置决定)随增益K变化的轨迹来分析系统特性。 + +**核心原理:** +- 极点位置决定系统响应特性 +- 左半平面极点 → 稳定响应(衰减) +- 虚轴上极点 → 临界稳定(等幅振荡) +- 右半平面极点 → 不稳定响应(发散) + +#### 2.3.2 操作步骤 + +1. **切换到「🎯 根轨迹」标签页** +2. **输入开环传递函数系数** +3. **调整增益** + - 拖动滑块从 $K = 10^{-4}$ 到 $K = 10^{4}$ + - 观察红色×标记的极点移动轨迹 +4. **分析极点位置** + +#### 2.3.3 图表解读 + +| 元素 | 含义 | +|------|------| +| 蓝色曲线 | 根轨迹(K从0到∞时闭环极点的轨迹) | +| 红色× | 当前增益K下的闭环极点位置 | +| 虚线 | 阻尼比等值线($\zeta = 0.707$ 对应 $\cos^{-1}(0.707) \approx 45°$) | + +#### 2.3.4 稳定性判断 + +| 极点位置 | 系统状态 | 响应特征 | +|----------|----------|----------| +| 左半平面 | 稳定 | 响应最终衰减 | +| 虚轴上 | 临界稳定 | 持续等幅振荡 | +| 右半平面 | 不稳定 | 响应发散 | + +#### 2.3.5 根轨迹的基本性质 + +| 性质 | 描述 | +|------|------| +| 起点 | K=0时,极点位于开环传递函数的极点 | +| 终点 | K→∞时,极点趋向开环传递函数的零点 | +| 分支数 | 等于系统阶数(分母阶数) | +| 渐近线 | 趋向无穷远处的分支沿渐近线分布 | +| 分离点 | 根轨迹在实轴上分离/会合的点 | + +#### 2.3.6 学习建议 + +**练习方向:** +1. 绘制并验证根轨迹的起点、终点、渐近线 +2. 找出根轨迹的分离点位置 +3. 确定系统稳定的增益K的范围 + +--- + +### 2.4 算例演示 + +#### 2.4.1 模块概述 + +算例演示模块是本平台的特色功能,通过完整的串联式混合动力系统模型,展示控制系统设计在实际工程中的应用。该模块采用四阶段设计,从模型训练到控制器设计再到能量管理,完整覆盖了控制系统设计的核心流程。 + +**四阶段架构:** + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 阶段零:模型训练 │ +│ ├── A. GPR模型训练/加载(数据驱动建模) │ +│ └── B. NN模型训练(知识蒸馏,实时仿真加速) │ +├─────────────────────────────────────────────────────────────┤ +│ 阶段一:发动机控制器设计 │ +│ ├── PID控制器(经典三参数控制) │ +│ └── MPC控制器(模型预测控制) │ +├─────────────────────────────────────────────────────────────┤ +│ 阶段二:电机控制器设计 │ +│ ├── PID控制器 │ +│ └── MPC控制器 │ +├─────────────────────────────────────────────────────────────┤ +│ 阶段三:能量管理策略设计 │ +│ └── 规则型EMS + 滞环控制 + 完整混动系统仿真 │ +└─────────────────────────────────────────────────────────────┘ +``` + +#### 2.4.2 阶段零:模型训练 + +**目的:** 建立发动机稳态代理模型,用于后续实时控制仿真。 + +**A. GPR模型训练/加载:** + +GPR(高斯过程回归)是一种数据驱动的建模方法,适合小样本、高维插值。 + +| 模式 | 说明 | 适用场景 | +|------|------|----------| +| load(推荐) | 加载已有模型权重 | 快速开始,无需等待 | +| train | 从CSV数据从头训练 | 修改了训练数据后 | + +**B. NN模型训练(知识蒸馏):** + +将GPR模型的知识蒸馏到轻量级神经网络中,牺牲少量精度换取推理速度的大幅提升。 + +| 参数 | 说明 | 推荐值 | +|------|------|--------| +| 训练轮数 | 越多越精确,但耗时更长 | 3000 | +| 学习率 | 控制收敛速度 | 3e-3 | +| 隐藏层宽度 | MLP每层神经元数 | 64 | + +#### 2.4.3 阶段一:发动机控制器设计 + +**目的:** 设计涡轴发动机燃油控制器,使发动机功率快速、准确地跟踪目标指令。 + +**被控对象模型:** + +``` +输入: [高度H, 马赫数Ma, 转速N] + ↓ + NN/GPR代理模型 + ↓ +输出: [燃油流量Wf, 功率P] +``` + +**动态特性方程:** +$$\tau_f \frac{dW_f}{dt} + W_f = W_{f,cmd} \quad \text{(燃油执行机构)}$$ +$$T \frac{dN}{dt} = K(W_f - W_{f,eq}(N)) \quad \text{(转子动力学)}$$ + +**PID控制器参数:** + +| 参数 | 作用 | 调节建议 | +|------|------|----------| +| Kp(比例增益) | 加快响应速度 | 过大导致振荡 | +| Ki(积分增益) | 消除稳态误差 | 过大致超调 | +| Kd(微分增益) | 抑制振荡 | 过小响应迟缓 | + +**MPC控制器参数:** + +| 参数 | 作用 | 调节建议 | +|------|------|----------| +| 预测时域 | 前看步数,越长越激进 | 15步左右 | +| W_power | 功率跟踪权重 | 越大跟踪越紧 | +| W_dcost | 控制增量权重 | 越大控制越平缓 | +| 超调限制 | 功率超调硬约束 | 5% | + +#### 2.4.4 阶段二:电机控制器设计 + +**目的:** 设计驱动电机转速控制器,使电机转速跟踪目标指令,并具备抗负载扰动能力。 + +**被控对象:** 永磁同步电机(PMSM) + +**关键方程:** +$$T_e = 1.5n_p[\psi_f + (L_d - L_q)i_d]i_q \quad \text{(电磁转矩)}$$ +$$J\frac{d\omega}{dt} = T_e - T_L - B\omega \quad \text{(机械方程)}$$ + +**负载扰动测试:** +- 在仿真60%时刻自动施加150%额定负载 +- 用于检验控制器的抗扰动能力 + +#### 2.4.5 阶段三:能量管理策略设计 + +**目的:** 设计混合动力系统的能量管理策略(EMS),协调发动机、电机和电池的功率分配。 + +**系统架构:** + +``` + ┌──────────────┐ + │ 发动机 │ + │ (发电机组) │ + └──────┬───────┘ + │ 发电功率 P_gen + ▼ +┌────────────┐ ┌──────────────┐ ┌──────────────┐ +│ 电池组 │◄──►│ 直流母线 │◄──►│ 驱动电机 │ +│ (储能) │ │ (功率枢纽) │ │ (推进) │ +└────────────┘ └──────────────┘ └──────────────┘ +``` + +**SOC滞环控制策略:** + +| SOC区间 | 工作模式 | 发动机行为 | +|---------|----------|-----------| +| $< SOC_{low}$ | 充电模式 | 固定输出 $P_{charge}$ | +| $> SOC_{high}$ | 功率跟随 | 输出 = 电机需求 + 储备 + 补偿 | +| 滞环区间内 | 保持 | 维持前一模式 | + +**输出图表解读:** + +| 图表 | 内容 | 关注点 | +|------|------|--------| +| 推进轴转速 | 目标转速 vs 实际转速 | 跟踪误差 | +| 功率分配 | 电机需求/发动机输出/电池功率 | 功率流向 | +| 电气状态 | 母线电压与SOC | 电池状态 | +| 燃油消耗 | 燃油流量时间曲线 | 经济性 | + +**关键时刻数据表:** + +系统自动选取8个关键时刻点,展示完整的状态数据,便于分析关键时刻的系统行为。 + +--- + +### 2.5 AI智能问答 + +#### 2.5.1 功能说明 + +AI智能问答模块集成了DeepSeek API,提供24小时在线的自动控制理论学习助手,支持LaTeX公式渲染和多轮对话上下文记忆。 + +#### 2.5.2 操作步骤 + +1. **切换到「🤖 智能问答」标签页** +2. **在底部输入框中输入问题** +3. **点击「📤 发送」按钮或按Enter键** +4. **等待AI回复**(支持流式输出,逐字显示) + +#### 2.5.3 推荐问题类型 + +**概念解释类:** +- "请解释传递函数的定义和物理意义" +- "什么是系统的稳态误差?" +- "Bode图中的增益裕度和相位裕度是什么?" + +**公式推导类:** +- "如何推导二阶系统的超调量公式?" +- "根轨迹的渐近线如何计算?" +- "请推导PID控制器的离散化公式" + +**例题分析类:** +- "如何用劳斯判据判断这个系统的稳定性?" +- "请分析这个传递函数的阶跃响应特性" +- "帮我求这个系统的稳态误差" + +**参数分析类:** +- "PID控制器中三个参数分别如何影响系统响应?" +- "阻尼比大于1和小于1有什么区别?" +- "MPC的预测时域对控制效果有什么影响?" + +#### 2.5.4 提问技巧 + +**✅ 推荐的提问方式:** +- 问题描述清晰,包含具体参数 +- 指出自己已经理解的方面 +- 说明学习的上下文(如"正在学习根轨迹法") + +**❌ 应避免的提问方式:** +- 过于笼统("帮我做题") +- 缺乏上下文("这个对吗?"而不给出具体内容) +- 语法不通(影响AI理解) + +#### 2.5.5 公式显示 + +AI回复中的数学公式会自动渲染,支持以下格式: + +| 格式类型 | 写法 | 显示效果 | +|----------|------|----------| +| 行内公式 | `$公式$` | 行内显示 | +| 独立公式 | `$$公式$$` | 居中显示 | +| 上标 | `s^{2}` | $s^{2}$ | +| 下标 | `a_1` | $a_1$ | +| 分数 | `\frac{a}{b}` | $\frac{a}{b}$ | +| 根号 | `\sqrt{x}` | $\sqrt{x}$ | + +--- + +## 三、传递函数示例库 + +### 3.1 一阶系统 + +**标准形式:** $G(s) = \frac{K}{\tau s + 1}$ + +**示例:** $G(s) = \frac{1}{s + 1}$ + +``` +分子系数: 1 +分母系数: 1,1 +``` + +**特性:** 无超调,响应单调上升,上升时间 $\approx 2.2\tau$ + +--- + +### 3.2 二阶欠阻尼系统 + +**标准形式:** $G(s) = \frac{\omega_n^2}{s^2 + 2\zeta\omega_n s + \omega_n^2}$,其中 $0 < \zeta < 1$ + +**示例($\zeta = 0.3, \omega_n = 1$):** $G(s) = \frac{1}{s^2 + 0.6s + 1}$ + +``` +分子系数: 1 +分母系数: 1,0.6,1 +``` + +**特性:** 有超调,振荡衰减,响应快速 + +--- + +### 3.3 二阶临界阻尼系统 + +**标准形式:** $G(s) = \frac{\omega_n^2}{(s + \omega_n)^2}$ + +**示例:** $G(s) = \frac{1}{s^2 + 2s + 1}$ + +``` 分子系数: 1 分母系数: 1,2,1 ``` -这是一个典型的二阶系统:$G(s) = \frac{1}{s^2+2s+1}$ +**特性:** 最快无振荡响应,无超调 --- -### 3.2 频域分析 +### 3.4 二阶过阻尼系统 -#### 3.2.1 功能说明 +**标准形式:** $G(s) = \frac{\omega_n^2}{(s + \omega_n)(s + \zeta\omega_n)}$,其中 $\zeta > 1$ -通过Bode图和Nyquist图分析系统的频率特性和稳定性。 +**示例:** $G(s) = \frac{1}{s^2 + 3s + 2}$ -#### 3.2.2 使用步骤 +``` +分子系数: 1 +分母系数: 1,3,2 +``` -1. 点击顶部"频域分析"标签页 -2. 输入传递函数的分子和分母系数(同时域分析) -3. 拖动"对数增益 log₁₀(K)"滑块,调整系统增益 -4. 观察右侧的Bode图和Nyquist图实时变化 - -#### 3.2.3 输出结果 - -- **Bode图**:幅频特性和相频特性曲线 -- **Nyquist图**:极坐标表示的频率响应 -- **稳定裕度**:增益裕度(GM)和相角裕度(PM) -- **稳定性评估**:系统是否稳定的判断结果 - -#### 3.2.4 稳定性判断标准 - -- GM > 0 dB 且 PM > 0° → **系统稳定** -- GM < 0 dB 或 PM < 0° → **系统不稳定** +**特性:** 无超调,响应较慢,无振荡 --- -### 3.3 根轨迹分析 +### 3.5 二阶无阻尼系统 -#### 3.3.1 功能说明 +**标准形式:** $G(s) = \frac{\omega_n^2}{s^2 + \omega_n^2}$ -观察增益K从0到∞变化时,闭环极点在s平面上的移动轨迹。 +**示例:** $G(s) = \frac{4}{s^2 + 4}$ -#### 3.3.2 使用步骤 +``` +分子系数: 4 +分母系数: 1,0,4 +``` -1. 点击顶部"根轨迹"标签页 -2. 输入开环传递函数的分子和分母系数 -3. 拖动"对数增益 log₁₀(K)"滑块 -4. 观察极点位置的实时变化 - -#### 3.3.3 输出结果 - -- **根轨迹图**:蓝色曲线表示极点运动轨迹 -- **当前极点**:红色×标记表示当前增益K下的闭环极点位置 -- **极点坐标**:实部和虚部的具体数值 - -#### 3.3.4 稳定性判断标准 - -- 极点在左半平面(实部 < 0)→ **稳定** -- 极点在虚轴上(实部 = 0)→ **临界稳定** -- 极点在右半平面(实部 > 0)→ **不稳定** +**特性:** 等幅振荡,周期 $T = \frac{2\pi}{\omega_n}$ --- -### 3.4 算例演示 +### 3.6 三阶系统 -#### 3.4.1 功能说明 +**示例:** $G(s) = \frac{1}{s^3 + 6s^2 + 11s + 6}$ -通过完整混动模型进行系统级算例仿真,输出图文结果用于教学演示与参数对比。 +``` +分子系数: 1 +分母系数: 1,6,11,6 +``` -#### 3.4.2 使用步骤 - -1. 点击顶部"算例演示"标签页 -2. 设置工况模板与参数: - - 仿真时长、仿真步长 - - 初始SOC、初始发动机功率 - - 目标转速缩放系数、负载转矩缩放系数 -3. 点击"运行混动算例" -4. 查看三联图、结果解读与关键时刻数据表 - -#### 3.4.3 输出结果 - -- **推进轴转速响应图**:目标与实际转速跟踪 -- **功率分配图**:电机需求、发动机输出、电池功率 -- **电气状态图**:母线电压与SOC -- **结果解读**:最大转速误差、SOC变化、平均功率、平均燃油流量 - -#### 3.4.4 结果解释要点 - -- 仿真时长是模型时间,不等于程序实际等待时间 -- 电池功率正值表示放电,负值表示充电 -- 输入超限时会自动裁剪到安全范围 +**特性:** 可分解为三个一阶环节的串联,响应由最慢的极点主导 --- -### 3.5 AI智能问答 +### 3.7 带零点二阶系统 -#### 3.4.1 功能说明 +**示例:** $G(s) = \frac{s + 2}{s^2 + 3s + 2}$ -随时向AI助教提问自动控制理论相关的问题。 +``` +分子系数: 1,2 +分母系数: 1,3,2 +``` -#### 3.4.2 使用步骤 +**特性:** 零点会加快系统响应,增加超调量 -1. 点击顶部"智能问答"标签页 -2. 在底部输入框输入你的问题 -3. 点击"发送"按钮或按Enter键 -4. 等待AI助教回复(支持LaTeX公式显示) +--- -#### 3.4.3 可提问的内容 +### 3.8 延迟环节系统 -- **概念解释**:"什么是传递函数?" -- **公式推导**:"如何计算二阶系统的超调量?" -- **例题讲解**:"如何用劳斯判据判断稳定性?" -- **知识点讨论**:"PID控制器的三个参数分别有什么作用?" +**示例:** $G(s) = \frac{e^{-0.5s}}{s + 1}$ -#### 3.4.4 示例问题 +**近似方法:** 帕德近似 +$$e^{-\tau s} \approx \frac{1 - \frac{\tau}{2}s}{1 + \frac{\tau}{2}s}$$ -```text -1. 什么是增益裕度和相角裕度? -2. 如何从根轨迹图判断系统的稳定性? -3. 解释一下Nyquist稳定判据 -4. 二阶系统的阻尼比对响应有什么影响? +**输入:** +``` +分子系数: 1,-2 +分母系数: 1,2,2 ``` --- -## 四、常用传递函数示例 +## 四、学习路径建议 -### 4.1 一阶系统 +### 4.1 初学者路径(第1-2周) -```text -分子: 1 -分母: 1,1 -``` +**目标:** 理解基本概念,会使用时域分析工具 -传递函数:$G(s) = \frac{1}{s+1}$ +**建议步骤:** +1. 阅读「时域分析」模块的知识卡片 +2. 尝试输入不同阻尼比的二阶系统,观察响应差异 +3. 理解上升时间、峰值时间、超调量、调节时间的物理意义 +4. 向AI助手提问,澄清疑惑 -### 4.2 二阶欠阻尼系统 - -```text -分子: 1 -分母: 1,2,1 -``` - -传递函数:$G(s) = \frac{1}{s^2+2s+1}$ - -### 4.3 二阶无阻尼系统 - -```text -分子: 4 -分母: 1,0,4 -``` - -传递函数:$G(s) = \frac{4}{s^2+4}$ - -### 4.4 三阶系统 - -```text -分子: 1 -分母: 1,6,11,6 -``` - -传递函数:$G(s) = \frac{1}{s^3+6s^2+11s+6}$ +**推荐练习:** +- 绘制 $\zeta = 0.1, 0.3, 0.5, 0.7, 1.0$ 的二阶系统响应 +- 观察阻尼比对超调量的影响 +- 记录数据,绘制 $\zeta$ vs $\sigma\%$ 曲线 --- -## 五、使用技巧与建议 +### 4.2 进阶学习路径(第3-4周) -### 5.1 推荐做法 +**目标:** 掌握频域分析方法,理解稳定性裕度 -1. **先显示传递函数** - 确认输入正确再进行分析 -2. **对比不同参数** - 修改系数观察系统特性变化 -3. **结合AI问答** - 不理解的地方随时提问 -4. **多尝试示例** - 从简单系统开始,逐步深入 +**建议步骤:** +1. 学习Bode图的基本绘制规则 +2. 使用频域分析工具绘制不同系统的Bode图 +3. 理解增益裕度和相位裕度的物理意义 +4. 分析增益K变化对稳定裕度的影响 -### 5.2 常见问题及解决方法 +**推荐练习:** +- 找出典型二阶系统的增益交越频率和相位交越频率 +- 计算并验证稳定裕度 +- 理解 "相位裕度越大,系统越稳定" 的直观含义 + +--- + +### 4.3 根轨迹专项路径(第5周) + +**目标:** 掌握根轨迹法,能设计简单控制器 + +**建议步骤:** +1. 理解根轨迹的基本规则(起点、终点、渐近线等) +2. 使用根轨迹分析工具观察极点移动规律 +3. 找出使系统稳定的增益范围 +4. 理解主导极点的概念 + +**推荐练习:** +- 对比开环极点位置与闭环极点位置的关系 +- 找出根轨迹的分离点 +- 确定临界增益(系统临界稳定时的K值) + +--- + +### 4.4 算例演示专项路径(第6-7周) + +**目标:** 通过混动系统算例,理解控制系统设计的完整流程 + +**建议步骤:** +1. 按顺序完成四个阶段的学习 +2. 理解GPR和NN模型的作用 +3. 对比PID和MPC控制器的效果差异 +4. 分析能量管理策略对SOC的影响 + +**推荐练习:** +- 调节PID参数,观察发动机功率跟踪效果 +- 对比不同预测时域下MPC的控制效果 +- 分析SOC滞环参数对充放电频率的影响 + +--- + +## 五、常见问题与解决方案 + +### 5.1 输入与格式问题 **问题1:输入后没有反应** -- 检查输入格式:系数之间用英文逗号分隔 -- 确保分子和分母都已输入 +**可能原因:** +- 分子或分母输入为空 +- 系数格式错误(使用了中文逗号) +- 分母阶数低于分子阶数(物理不可实现) + +**解决方法:** +1. 检查输入格式,确保使用英文逗号分隔 +2. 确认分子和分母都已填写 +3. 确保分母阶数 ≥ 分子阶数 +4. 刷新页面后重试 + +--- **问题2:图表显示异常** -- 刷新页面重试 -- 检查传递函数的阶数是否合理(分母阶数≥分子阶数) +**可能原因:** +- 传递函数系数导致数值不稳定 +- 系统不稳定导致响应发散 -**问题3:AI回答不准确** - -- 尝试更具体地描述问题 -- 问题中包含关键词和背景信息 - -**问题4:在线人数显示说明** - -- 页面顶部显示当前有多少同学在线使用平台 -- 每10秒自动更新 +**解决方法:** +1. 尝试简化传递函数系数 +2. 检查系统是否稳定(不稳定系统的响应会发散) +3. 刷新页面重试 --- -## 六、学习建议 +### 5.2 算例演示问题 -### 6.1 时域分析练习方向 +**问题3:提示 "engine_nn_proxy.pth not found"** -1. 对比一阶和二阶系统的响应差异 -2. 观察阻尼比对超调量的影响 -3. 分析零点对系统响应的作用 +**原因:** 尚未完成阶段零的NN模型训练 -### 6.2 频域分析练习方向 - -1. 调整增益K,观察稳定裕度变化 -2. 找出系统临界稳定的增益值 -3. 理解Bode图和Nyquist图的关系 - -### 6.3 根轨迹分析练习方向 - -1. 找出根轨迹的分离点 -2. 确定系统稳定的增益范围 -3. 观察主导极点的移动规律 +**解决方法:** +1. 切换到「阶段零:模型训练」 +2. 在「NN模型训练」子标签页中 +3. 设置参数后点击「🚀 开始 NN 训练」 +4. 等待训练完成(约2-5分钟) --- -## 七、系统要求 +**问题4:GPR训练失败** -### 7.1 设备要求 +**原因:** 缺少 botorch/gpytorch 依赖 -- **浏览器**: Chrome 90+、Edge 90+、Firefox 88+ -- **网络**: 稳定的互联网连接 -- **屏幕分辨率**: 建议 1366×768 及以上 +**解决方法(选择一种):** +1. **安装依赖:** + ```bash + pip install botorch gpytorch scikit-learn + ``` -### 7.2 网络要求 - -- 可访问校园网或互联网 -- 建议带宽不低于2Mbps +2. **使用load模式(推荐):** + - 在GPR子标签页中选择「load」模式 + - 加载已有的模型权重文件 --- -## 八、技术支持 +**问题5:仿真运行缓慢** -**联系方式**: +**可能原因:** +- 仿真步长设置过小 +- 仿真时长设置过长 +- CPU性能不足 -- 邮箱: pengfeiwei@nwpu.edu.cn -- 机构: 西北工业大学 -- 负责人: 魏鹏飞 - -**说明**: - -- 课程相关问题:请联系任课教师或助教 -- 平台技术问题:请通过上述邮箱反馈 +**解决方法:** +1. 增大仿真步长(如从0.02改为0.05或0.1) +2. 缩短仿真时长 +3. 减少预测时域(MPC控制器) --- -## 九、附录 +### 5.3 AI问答问题 -### 9.1 平台特色功能 +**问题6:AI回答不准确** -1. **实时在线人数统计** - 页面顶部显示当前在线用户数 -2. **图文联动演示** - 算例演示支持图、表、文本同步展示 -3. **LaTeX公式支持** - 完美显示数学公式 -4. **响应式设计** - 适配不同屏幕尺寸 +**可能原因:** +- 问题描述不够具体 +- 缺少必要的上下文信息 -### 9.2 更新说明 +**解决方法:** +1. 提供具体的传递函数参数 +2. 说明自己已经理解的方面 +3. 明确指出困惑的具体点 -- 版本: v1.2.0 -- 更新日期: 2026年4月7日 -- 主要更新: 新增算例演示模块与完整混动模型联动 +**示例对比:** + +❌ 不好的问题: +``` +"这个系统怎么分析?" +``` + +✅ 好的问题: +``` +"对于传递函数 G(s) = 1/(s^2+0.6s+1), +我知道这是一个欠阻尼二阶系统, +但我不理解为什么阻尼比 ζ=0.3 时超调量会达到约 37%, +请帮我推导超调量的计算公式。" +``` + +--- + +**问题7:公式不渲染** + +**原因:** Chatbot组件未正确配置LaTeX + +**解决方法:** +这是已知的配置问题,不影响AI回复内容的正确性。可以尝试: +1. 刷新页面 +2. 简化公式写法 + +--- + +### 5.4 系统问题 + +**问题8:页面加载缓慢** + +**可能原因:** +- 网络连接不稳定 +- 浏览器缓存过多 + +**解决方法:** +1. 尝试刷新页面 +2. 清除浏览器缓存 +3. 更换浏览器 + +--- + +**问题9:在线人数显示为0或异常** + +**说明:** +- 首次访问会创建新的会话ID +- 在线统计基于浏览器会话,非登录用户 +- 每10秒自动刷新一次 + +**解决方法:** +1. 确认页面已加载超过10秒 +2. 刷新页面重置会话 + +--- + +## 六、参考资料 + +### 6.1 教材与参考书 + +| 书名 | 作者 | 出版社 | 适合阶段 | +|------|------|--------|----------| +| 自动控制原理(第七版) | 胡寿松 | 科学出版社 | 教材,入门必读 | +| Modern Control Engineering | Katsuhiko Ogata | Prentice Hall | 经典英文教材 | +| Modern Control Systems | Richard C. Dorf | Pearson | 全面覆盖 | +| Feedback Control Theory | John Doyle et al. | Macmillan | 理论深入 | + +### 6.2 在线资源 + +| 资源 | 链接 | 说明 | +|------|------|------| +| python-control文档 | https://python-control.readthedocs.io/ | 库函数参考 | +| DeepSeek API | https://platform.deepseek.com/docs | API配置参考 | +| Gradio文档 | https://gradio.app/docs | 界面框架参考 | + +--- + +## 七、技术支持 + +### 7.1 联系方式 + +| 项目 | 信息 | +|------|------| +| 项目负责人 | 魏鹏飞 | +| 电子邮件 | pengfeiwei@nwpu.edu.cn | +| 机构 | 西北工业大学 | +| 课程相关问题 | 请联系任课教师或助教 | + +### 7.2 问题反馈 + +反馈时请包含以下信息: +1. **问题描述**:具体描述遇到的问题 +2. **操作步骤**:您进行了哪些操作 +3. **错误信息**:截图或完整的错误提示 +4. **环境信息**:浏览器版本、操作系统 + +--- + +## 八、附录 + +### 8.1 快捷操作汇总 + +| 操作 | 方法 | +|------|------| +| 发送问题 | 点击「📤 发送」按钮 或 按 Enter 键 | +| 清空对话 | 点击「🗑️ 清空」按钮 | +| 快速输入 | 点击示例问题自动填入 | +| 调整增益 | 拖动滑块 或 直接输入数字 | +| 显示传递函数 | 点击「✓ 显示传递函数」按钮 | + +### 8.2 版本信息 + +| 项目 | 信息 | +|------|------| +| 当前版本 | v1.2.0 | +| 更新日期 | 2026年4月7日 | +| 主要更新 | 新增算例演示模块与完整混动模型联动 | + +--- + +**使用过程中如有疑问,欢迎通过上述联系方式反馈!** + +祝学习顺利! diff --git a/README.md b/README.md index a97c476..4fd8215 100644 --- a/README.md +++ b/README.md @@ -1,218 +1,516 @@ -# 🎛️ 自动控制原理AI+数智平台 +# 自动控制理论AI+数智平台 > 交互式控制系统分析与设计工具 | 时域·频域·根轨迹·算例演示·AI问答 -一个基于 Gradio 构建的现代化自动控制原理学习平台,集成了系统分析工具、完整混动模型算例演示与 AI 智能问答功能。 +[![Python Version](https://img.shields.io/badge/Python-3.10%2B-blue)](https://www.python.org/) +[![Gradio](https://img.shields.io/badge/Gradio-4.44.1-orange)](https://gradio.app/) +[![License](https://img.shields.io/badge/License-MIT-green)](LICENSE) -## ✨ 核心功能 +## 项目简介 -### 📊 1. 时域分析 (Time Domain Analysis) +自动控制理论AI+数智平台是一个基于 Gradio 构建的现代化自动控制原理学习平台,集成了系统分析工具、完整混动模型算例演示,和AI智能问答功能。本项目为西北工业大学2025年校级本科生建设项目成果。 -- **阶跃响应分析**:观察系统对单位阶跃输入的响应 -- **脉冲响应分析**:观察系统对单位脉冲输入的响应 -- **性能指标计算**: - - 上升时间 (Rise Time) - - 峰值时间 (Peak Time) - - 超调量 (Overshoot) - - 调节时间 (Settling Time) - - 稳态值 (Steady State Value) +平台旨在为自动控制理论课程提供交互式、数智化的学习环境,使学生能够直观理解控制系统的时域响应、频域特性、根轨迹分析等核心概念,并通过完整的混合动力系统算例演示,将理论知识与工程实践相结合。 -**知识要点**: -- 二阶系统标准形式 -- 阻尼比与自然频率参数说明 -- 时域性能指标公式 -- 稳态误差分析 +--- -### 🌊 2. 频域分析 (Frequency Domain Analysis) +## 核心功能 -- **Bode 图绘制**:幅频特性和相频特性 -- **Nyquist 图绘制**:极坐标频率响应 -- **稳定裕度计算**: - - 增益裕度 (Gain Margin, GM) - - 相位裕度 (Phase Margin, PM) - - 增益交越频率 - - 相角交越频率 -- **稳定性评估**:自动判断系统稳定性 +本平台提供五大核心功能模块,涵盖经典控制理论分析与现代控制系统设计: -**知识要点**: -- 增益裕度与相位裕度定义 -- 稳定性判断准则 -- 频域指标与时域性能的关系 -- Bode 图读数技巧 +### 1. 时域分析 (Time Domain Analysis) -### 🎯 3. 根轨迹分析 (Root Locus Analysis) +时域分析是研究控制系统在时间域内对输入信号响应特性的方法,是自动控制理论的基础分析方法之一。 -- **根轨迹绘制**:自动绘制完整根轨迹图 -- **增益调节**:对数滑块精确调节增益 K -- **极点跟踪**:实时显示当前增益下的闭环极点位置 -- **动态视角**:自动调整坐标范围,聚焦关键区域 +**主要功能:** +- **阶跃响应分析**:观察系统对单位阶跃输入的响应,这是控制系统分析中最常用的测试信号 +- **脉冲响应分析**:观察系统对单位脉冲(δ函数)输入的响应,用于分析系统的固有特性 +- **性能指标计算**:自动计算并显示系统的关键时域性能指标 -**知识要点**: -- 根轨迹法基本原理 -- 幅值条件与相角条件 -- 根轨迹的基本性质(起点、终点、渐近线、分离点) -- s 平面稳定性区域 -- 阻尼比等值线 +**计算的性能指标:** +| 指标 | 符号 | 定义 | 物理意义 | +|------|------|------|----------| +| 上升时间 | $t_r$ | 响应从稳态值的10%上升到90%所需时间 | 反映系统响应速度 | +| 峰值时间 | $t_p$ | 响应达到第一个峰值的时间 | 反映系统阻尼特性 | +| 超调量 | $\sigma\%$ | 峰值超过稳态值的百分比 | 反映系统振荡程度 | +| 调节时间 | $t_s$ | 响应进入并保持在稳态值±2%区间的时间 | 反映系统 settling 速度 | +| 稳态值 | $y(\infty)$ | 时间趋于无穷时系统的输出值 | 反映系统最终行为 | -### 🧪 4. 算例演示 (Case Demo) +**二阶系统标准形式:** -- **完整混动模型**:调用 `Model/src/series_hybrid_sim.py` 进行系统级仿真 -- **参数化工况**:支持工况模板、仿真时长、步长、SOC、初始发动机功率与缩放系数 -- **图文结果**:输出三联图(转速响应/功率分配/电气状态)与关键数据表 -- **教学解读**:自动生成指标摘要(最大转速误差、SOC变化、平均功率与燃油流量) +二阶系统是自动控制理论中最重要的系统类型,其标准传递函数为: -### 🤖 5. AI 智能问答 (Q&A) +$$G(s) = \frac{\omega_n^2}{s^2 + 2\zeta\omega_n s + \omega_n^2}$$ +其中: +- $\omega_n$ — 自然频率(rad/s),表示系统无阻尼时的固有振荡频率 +- $\zeta$ — 阻尼比,表示系统阻尼程度的相对量 + +**阻尼比与系统响应关系:** + +| 阻尼比范围 | 系统类型 | 响应特性 | +|------------|----------|----------| +| $\zeta = 0$ | 无阻尼 | 持续等幅振荡 | +| $0 < \zeta < 1$ | 欠阻尼 | 振荡衰减,响应快速 | +| $\zeta = 1$ | 临界阻尼 | 最快无振荡响应 | +| $\zeta > 1$ | 过阻尼 | 无振荡,但响应较慢 | + +**稳态误差分析:** + +稳态误差是系统长期运行后实际输出与期望输出之间的差值,是评价系统控制精度的重要指标。对于单位反馈系统: + +$$e_{ss} = \lim_{t\to\infty} e(t) = \lim_{s\to0} \frac{R(s)}{1+G(s)H(s)}$$ + +--- + +### 2. 频域分析 (Frequency Domain Analysis) + +频域分析通过研究系统对不同频率正弦信号的响应特性来分析系统性能,是经典控制理论的核心方法之一。 + +**主要功能:** +- **Bode 图绘制**:同时显示幅频特性曲线和相频特性曲线 +- **Nyquist 图绘制**:以极坐标形式展示系统频率响应 +- **稳定裕度计算**:定量评估系统的相对稳定性 + +**Bode 图(波特图):** + +Bode 图包含两个子图: +- **幅频特性图**:显示增益(单位:dB)随频率变化的关系 +- **相频特性图**:显示相位(单位:度)随频率变化的关系 + +对数频率特性的优点: +1. 可以将频率范围压缩,便于观察宽频带内的特性 +2. 幅值相乘转化为对数相加,简化串联系统计算 +3. 渐近线近似作图简便实用 + +**Nyquist 稳定判据:** + +Nyquist 图是基于复变函数理论的稳定性判据。对于闭环系统: + +$$T(s) = \frac{G(s)}{1 + G(s)}$$ + +稳定性条件:当 $\omega$ 从 $-\infty$ 变化到 $+\infty$ 时,$G(j\omega)H(j\omega)$ 轨迹顺时针包围 $(-1, j0)$ 点 $P$ 圈,其中 $P$ 为开环不稳定极点数。 + +**稳定裕度(Stability Margins):** + +| 裕度类型 | 定义 | 计算公式 | 经验要求 | +|----------|------|----------|----------| +| 增益裕度 GM | 相角为 $-180°$ 时,闭环增益还能增大多少 | $GM = \frac{1}{|G(j\omega_g)|}$ | $GM > 1.0$(即 $GM_{dB} > 0$) | +| 相位裕度 PM | 增益为1(0dB)时,相位还能滞后多少 | $PM = 180° + \angle G(j\omega_c)$ | $PM > 45°$ | + +**稳定性判断准则:** +- $GM > 0$ dB 且 $PM > 0°$ → **系统稳定** +- $GM < 0$ dB 或 $PM < 0°$ → **系统不稳定** + +**频域指标与时域性能的关系:** + +| 频域指标 | 时域对应 | 经验公式 | +|----------|----------|----------| +| 带宽 $\omega_b$ | 上升时间 $t_r$ | $t_r \approx \frac{1.8}{\omega_b}$ | +| 相位裕度 PM | 超调量 $\sigma\%$ | $\sigma\% \approx 100 \times e^{-\pi PM/(90-PM)}$ | +| 增益裕度 GM | 稳定余量 | GM 越大,系统对不确定性越不敏感 | + +--- + +### 3. 根轨迹分析 (Root Locus Analysis) + +根轨迹法是一种图解方法,用于分析系统闭环极点随增益 K 变化的轨迹,是控制系统设计的核心工具。 + +**主要功能:** +- **完整根轨迹绘制**:自动绘制开环增益从0到无穷变化时的闭环极点轨迹 +- **增益调节**:通过滑块精确调节增益 K,实时观察极点位置变化 +- **极点跟踪**:显示当前增益下闭环极点的精确位置 +- **动态坐标**:自动调整坐标系范围,聚焦关键区域 + +**根轨迹的基本规则:** + +1. **起点与终点**: + - 起点(K=0):开环传递函数的极点($n$ 个) + - 终点(K→∞):开环传递函数的零点($m$ 个),剩余 $n-m$ 个趋向无穷 + +2. **渐近线**: + - 当 $K \to \infty$ 时,根轨迹趋向 $n-m$ 条渐近线 + - 渐近线与实轴的夹角:$\phi_a = \frac{(2k+1)180°}{n-m}$ + +3. **分离点与会合点**: + - 根轨迹在实轴上相邻两分支之间的某点分离或会合 + - 分离点坐标可通过求解 $\frac{dK}{ds} = 0$ 得到 + +4. **与虚轴的交点**: + - 根轨迹与虚轴的交点对应的增益和频率可通过劳斯判据确定 + +**s平面稳定性区域:** + +| 极点位置 | 系统状态 | 物理意义 | +|----------|----------|----------| +| 左半平面(Re(s) < 0) | 稳定 | 响应最终衰减 | +| 虚轴(Re(s) = 0) | 临界稳定 | 持续振荡 | +| 右半平面(Re(s) > 0) | 不稳定 | 响应发散 | + +**阻尼比等值线:** + +在 s 平面上,阻尼比 $\zeta$ 等于常数的曲线是通过原点的射线。对于二阶系统: + +$$\zeta = \cos(\theta)$$ + +其中 $\theta$ 是该射线与负实轴的夹角。阻尼比越大,射线越接近负实轴,系统响应越平稳但越缓慢。 + +--- + +### 4. 算例演示 (Case Demo) + +算例演示模块是本平台的特色功能,通过完整的串联式混合动力系统模型,展示控制系统设计在实际工程中的应用。 + +**模块架构(四阶段设计):** + +``` +阶段零:模型训练 +├── GPR模型训练/加载(高斯过程回归) +└── NN模型训练(知识蒸馏) + +阶段一:发动机控制器设计 +├── PID控制器 +└── MPC控制器(模型预测控制) + +阶段二:电机控制器设计 +├── PID控制器 +└── MPC控制器(模型预测控制) + +阶段三:能量管理策略设计 +├── 规则型能量管理(基于SOC滞环) +└── 完整混动系统仿真 +``` + +**发动机模型(Engine Model):** + +基于高斯过程回归(GPR)和神经网络(NN)代理模型的涡轴发动机动态仿真: + +- **输入变量**:高度 $H$ (m)、马赫数 $Ma$、转速 $N$ (RPM) +- **输出变量**:燃油流量 $W_f$ (kg/h)、输出功率 $P$ (kW) + +**发动机动态特性:** +$$\tau_f \frac{dW_f}{dt} + W_f = W_{f,cmd}$$ +$$T \frac{dN}{dt} = K(W_f - W_{f,eq}(N))$$ + +其中 $\tau_f$ 是燃油执行机构时间常数,$K$ 是转子惯性增益。 + +**电机模型(Motor Model):** + +永磁同步电机(PMSM)离散时间动力学模型: + +- **d/q轴电流控制**:$i_d = 0$ 控制(MTPA) +- **转矩方程**:$T_e = 1.5n_p[\psi_f + (L_d - L_q)i_d]i_q$ +- **机械方程**:$J\frac{d\omega}{dt} = T_e - T_L - B\omega$ + +**电池模型(Battery Model):** + +等效电路模型,包含: +- **开路电压** OCV:与 SOC 相关的非线性查表 +- **内阻**:$R_{in} = 0.15 \Omega$ +- **SOC 更新**:安时积分法 + +$$SOC_{k+1} = SOC_k - \frac{I_k \cdot \Delta t}{C_{capacity}}$$ + +**能量管理策略(EMS):** + +基于规则的功率跟随策略,配合 SOC 滞环控制: + +| SOC 区间 | 工作模式 | 控制策略 | +|----------|----------|----------| +| $SOC < SOC_{low}$ | 充电模式 | 发动机输出固定功率 $P_{charge}$ | +| $SOC > SOC_{high}$ | 功率跟随 | 发动机输出 = 电机需求 + 储备功率 + SOC补偿 | +| 滞环区间内 | 保持 | 维持前一模式 | + +**MPC控制器(模型预测控制):** + +滚动时域优化控制器,核心思想: + +1. **预测模型**:利用系统线性化模型预测未来 H 步状态 +2. **优化目标**:$\min \sum_{k=1}^{H} [\|w_k - w_{ref}\|^2_{Q} + \|\Delta u_k\|^2_{R}]$ +3. **约束处理**:执行机构限幅、超调量硬约束 + +本平台使用纯 Python 实现的投影梯度下降求解器,替代传统的 SLSQP / GEKKO 方案,避免 Fortran ABI 兼容性问题。 + +**PID控制器(增量式):** + +增量式 PID 控制器公式: + +$$\Delta u(k) = k_p[e(k) - e(k-1)] + k_i e(k) + k_d[e(k) - 2e(k-1) + e(k-2)]$$ + +特点: +- 计算增量而非绝对量,避免积分饱和 +- 支持输入/输出量程归一化,便于参数调节 + +--- + +### 5. AI 智能问答 (Q&A) + +AI 问答模块集成了 DeepSeek API,提供24小时在线的自动控制理论学习助手。 + +**主要功能:** - **专业教学助手**:精通自动控制原理的 AI 教授 -- **流式响应**:实时显示 AI 回复过程 +- **流式响应**:实时逐字显示 AI 回复,支持多轮对话 - **LaTeX 公式渲染**:完美支持数学公式显示 -- **上下文记忆**:支持多轮对话 +- **上下文记忆**:支持多轮连续对话,理解对话上下文 -**支持的 API**: -- DeepSeek API +**支持的 API:** +| API 提供商 | 模型选择 | 特点 | +|------------|----------|------| +| DeepSeek | deepseek-chat / deepseek-coder | 国内访问,中文优化 | -## 🚀 快速开始 +**提问技巧:** -### 环境要求 +✅ **推荐的问题类型:** +- 概念解释:"请解释传递函数的定义和物理意义" +- 公式推导:"如何推导二阶系统的超调量公式?" +- 例题讲解:"如何用劳斯判据判断这个系统的稳定性?" +- 参数分析:"PID控制器中三个参数分别如何影响系统响应?" -- Python 3.10+ -- pip 包管理器 +❌ **应避免的问题:** +- 过于宽泛:"帮我做作业"(建议具体描述问题) +- 缺乏上下文:"这个对吗?"(请提供具体系统参数) -### 安装步骤 +--- -1. **克隆项目** +## 系统要求 + +### 运行环境 + +| 项目 | 要求 | +|------|------| +| Python 版本 | 3.10+ | +| 操作系统 | Windows / Linux / macOS | +| 内存 | 建议 8GB 以上 | +| 显卡 | 可选(用于 GPR 训练加速,CPU 模式也可运行) | + +### 浏览器要求 + +推荐使用以下浏览器的最新版本以获得最佳体验: +- Google Chrome 90+ +- Microsoft Edge 90+ +- Mozilla Firefox 88+ +- Apple Safari 14+ + +--- + +## 快速开始 + +### 方式一:使用 pip 安装(推荐) + +**1. 克隆项目** ```bash git clone -cd AutoControl +cd AutoControlCourse ``` -2. **安装依赖** +**2. 创建虚拟环境(推荐)** ```bash -pip install -r requirements.txt -``` +# 使用 venv +python -m venv my_gradio_env +source my_gradio_env/bin/activate # Linux/macOS +# 或 +my_gradio_env\Scripts\activate # Windows -或使用 conda: - -```bash +# 或使用 conda conda create -n autocontrol python=3.10 conda activate autocontrol +``` + +**3. 安装依赖** + +```bash pip install -r requirements.txt ``` -3. **配置 API 密钥** +**4. 配置 API 密钥** -编辑 `config.py` 文件中的配置区域: +编辑 `config.py` 文件中的 API 配置区域: ```python # ==================== API 配置 ==================== API_KEY = "your-api-key-here" # 填入您的 DeepSeek API 密钥 API_BASE_URL = "https://api.deepseek.com/v1" API_MODEL = "deepseek-chat" -API_TYPE = "deepseek" # 或 "gemini" +API_TYPE = "deepseek" # ================================================== ``` -**获取 DeepSeek API 密钥**: -- 访问 https://platform.deepseek.com/api_keys -- 注册并创建 API 密钥 -- 复制密钥到配置文件 +**获取 DeepSeek API 密钥:** +1. 访问 https://platform.deepseek.com/api_keys +2. 注册并登录账号 +3. 点击"创建新密钥" +4. 复制生成的密钥并填入配置 -4. **运行应用** +**5. 运行应用** ```bash python app.py ``` -或使用 conda 环境: +**6. 访问应用** -```bash -conda run -n autocontrol python app.py -``` - -5. **访问应用** - -浏览器自动打开,或手动访问: +浏览器将自动打开,或手动访问: ``` http://localhost:7860 ``` -## 📖 使用指南 +### 方式二:使用 Docker(可选) -### 输入系统传递函数 +```bash +# 构建镜像 +docker build -t autocontrol-course . -在任意标签页的输入框中输入传递函数的分子和分母系数: - -**示例 1:一阶系统** -``` -分子: 1 -分母: 1,1 -传递函数: G(s) = 1/(s+1) +# 运行容器 +docker run -p 7860:7860 \ + -e API_KEY="your-api-key" \ + autocontrol-course ``` -**示例 2:二阶系统** +--- + +## 项目结构 + ``` -分子: 4 -分母: 1,2,4 -传递函数: G(s) = 4/(s²+2s+4) +AutoControlCourse/ +├── app.py # Gradio 主入口,事件绑定与界面布局 +├── ui_components.py # 各功能标签页的 UI 组件定义 +├── analysis_functions.py # 时域/频域/根轨迹分析核心计算函数 +├── case_demo_functions.py # 算例演示模块(混动模型四阶段仿真) +├── chatbot.py # AI 智能问答(DeepSeek API 集成) +├── user_stats.py # 在线人数统计与数据持久化 +├── config.py # 全局配置(API密钥、服务器端口等) +├── requirements.txt # Python 依赖列表 +│ +├── Model/ # 混动模型核心代码 +│ ├── src/ +│ │ ├── lightweight_model.py # NN代理模型(MLP,用于替代GPR) +│ │ ├── engine_gpr_class.py # GPR高斯过程回归模型 +│ │ ├── distill_gpr_to_nn.py # 知识蒸馏脚本(GPR→NN) +│ │ ├── engine_dynamic_sim.py # 涡轴发动机动态仿真 +│ │ ├── motor_sim.py # PMSM永磁同步电机仿真 +│ │ ├── battery_sim.py # 电池等效电路仿真 +│ │ ├── mpc_controller.py # MPC模型预测控制器 +│ │ ├── increPID.py # 增量式PID控制器 +│ │ └── series_hybrid_sim.py # 串联混动系统总成 +│ └── data/ +│ ├── Cleaned_Engine_Data_Full.csv # 发动机标定数据 +│ ├── engine_gpr_model.pth # GPR模型权重 +│ └── engine_nn_proxy.pth # NN代理模型权重 +│ +└── assets/ # 静态资源 + ├── styles.css # 自定义CSS样式 + └── knowledge_cards_html.py # 各模块知识卡片HTML内容 ``` -**示例 3:三阶系统** -``` -分子: 1 -分母: 1,6,11,6 -传递函数: G(s) = 1/(s³+6s²+11s+6) +--- + +## 技术栈 + +### 前端框架 +- **Gradio 4.44.1**:快速构建机器学习 Web 界面的 Python 库 +- **Custom CSS**:现代化样式定制,支持响应式布局 + +### 核心计算库 +| 库名 | 版本 | 用途 | +|------|------|------| +| NumPy | 1.26.4 | 数值计算基础库 | +| python-control | 0.9.4 | 控制系统分析工具箱 | +| Matplotlib | 3.9.4 | 图表绑制 | +| PyTorch | 2.4.1 | 神经网络推理(CPU模式) | + +### AI 集成 +| 库名 | 用途 | +|------|------| +| aiohttp | 异步HTTP请求,流式响应 | +| DeepSeek API | AI问答后端服务 | + +### 其他依赖 +| 库名 | 用途 | +|------|------| +| pandas | 数据处理(发动机CSV数据读取) | +| scikit-learn | 数据归一化预处理 | +| psutil | 系统资源监控(CPU/内存/显存) | + +--- + +## 算例演示模块详解 + +### 阶段零:模型训练 + +**GPR 模型训练/加载:** + +高斯过程回归是一种非参数概率模型,适合小样本、高维插值。 + +```python +# 运行模式 +mode = "load" # 加载已有模型(推荐) +mode = "train" # 从头训练(需要 botorch/gpytorch) ``` -**输入格式**: -- 系数从最高次项到常数项 -- 用逗号分隔(支持中文或英文逗号) -- 支持小数和负数 -- 例如:`1, 2.5, -3, 4` 表示 s³ + 2.5s² - 3s + 4 +**NN 模型训练(知识蒸馏):** -### 调节系统增益 +将 GPR 模型的知识蒸馏到轻量级 MLP 中,用于实时控制仿真。 -频域分析和根轨迹分析都支持增益调节: +关键参数: +- **训练轮数 (Epochs)**:越多越精确,推荐 3000 +- **学习率 (LR)**:推荐 1e-3 ~ 5e-3 +- **隐藏层宽度**:推荐 64(平衡精度与速度) -- **对数滑块**:log₁₀(K) 范围 -1 到 3 - - -1 对应 K = 0.1 - - 0 对应 K = 1 - - 1 对应 K = 10 - - 2 对应 K = 100 - - 3 对应 K = 1000 +### 阶段一:发动机控制器设计 -- **实时显示**:下方数字框显示实际增益值 -- **松开更新**:滑块松开后才更新图表,避免卡顿 +**PID 控制参数:** +- **Kp(比例增益)**:增大可加快响应,但过大导致振荡 +- **Ki(积分增益)**:消除稳态误差,过大导致超调 +- **Kd(微分增益)**:抑制振荡,改善动态特性 -### AI 问答技巧 +**MPC 控制参数:** +- **预测时域 (Horizon)**:前看步数,越长越激进 +- **功率跟踪权重 W_power**:越高则功率跟踪越紧 +- **控制增量权重 W_dcost**:越高则控制变化越平缓 +- **超调限制**:5% 硬约束 -**高效提问方式**: +### 阶段二:电机控制器设计 -✅ **好的问题**: -- "请解释 PID 控制器的三个参数如何影响系统性能?" -- "如何根据 Bode 图判断系统的稳定裕度?" -- "为什么阻尼比为 0.707 时系统响应最佳?" -- "推导二阶系统的超调量公式" +**仿真设置:** +- 仿真时长:5~60秒可调 +- 仿真步长:0.02s / 0.05s / 0.1s +- 目标转速:500~5000 RPM +- 负载转矩:10~400 Nm -❌ **避免的问题**: -- "帮我做作业"(太模糊) -- "这个对吗?"(缺少上下文) -- "答案是什么?"(没有提供问题描述) +**负载扰动测试:** +在仿真60%时刻自动施加150%负载扰动,检验控制器抗扰能力。 -**公式显示**: -AI 回复中的数学公式会自动渲染,支持以下格式: -- 行内公式:`$公式$` 或 `\(公式\)` -- 块级公式:`$$公式$$` 或 `\[公式\]` +### 阶段三:能量管理策略设计 -## 🎨 界面特色 +**SOC 滞环控制参数:** + +| 参数 | 含义 | 推荐值 | +|------|------|--------| +| SOC目标值 | 功率跟随模式下的补偿基准 | 60% | +| SOC下限阈值 | 低于此值进入充电模式 | 30% | +| SOC上限阈值 | 高于此值退出充电模式 | 70% | + +**功率规则参数:** + +| 参数 | 含义 | 推荐值 | +|------|------|--------| +| 发动机最小功率 | 最低运转功率 | 20 kW | +| 发动机最大功率 | 峰值输出功率 | 300 kW | +| 充电模式功率 | 充电时发动机输出 | 200 kW | +| SOC补偿增益 | SOC偏差修正力度 | 50 kW/ΔSOC | + +--- + +## 界面特色 ### 现代化设计 - **渐变色标题**:紫色渐变视觉效果 - **卡片式布局**:分组清晰,层次分明 -- **可滚动知识卡片**:长文档不占据过多空间 -- **可折叠章节**:按需展开知识点 +- **可滚动知识卡片**:长文档不占用过多屏幕空间 +- **可折叠章节**:按需展开,节省视线 ### 响应式交互 @@ -226,64 +524,15 @@ AI 回复中的数学公式会自动渲染,支持以下格式: - **嵌入式公式**:页面内直接显示 LaTeX 公式 - **表格对比**:性能指标、稳定准则表格化 - **颜色编码**:稳定/不稳定用绿/红色标识 -- **图标辅助**:emoji 增强视觉识别 +- **图标辅助**:Emoji 增强视觉识别 -## 🛠️ 技术栈 +--- -### 前端框架 -- **Gradio 4.x**:快速构建机器学习 Web 界面 -- **Custom CSS**:现代化样式定制 - -### 计算库 -- **NumPy**:数值计算 -- **python-control**:控制系统分析 -- **Matplotlib**:图表绘制 - -### AI 集成 -- **aiohttp**:异步 HTTP 请求 -- **DeepSeek API**:智能问答后端 -- **流式响应**:提升用户体验 - -## 📁 项目结构 - -```text -AutoControlCourse/ -├── app.py # Gradio 入口与事件绑定 -├── ui_components.py # 各标签页 UI 组件 -├── analysis_functions.py # 时域/频域/根轨迹计算 -├── case_demo_functions.py # 算例演示模块(调用 Model) -├── chatbot.py # AI 问答 -├── user_stats.py # 在线人数统计 -├── config.py # API 与运行配置 -├── Model/ -│ ├── src/ # 混动模型核心代码 -│ └── data/ # 运行所需模型权重与数据 -└── requirements.txt # 依赖列表 -``` - -## 🔧 高级配置 - -### 算例演示说明 - -- 仿真时长是模型时间,不等于程序实际等待时间 -- 电池功率符号定义:正值放电,负值充电 -- 若参数超限,程序会在安全范围内自动裁剪 - -### 自定义系统提示词 - -修改 `chat_with_ai` 函数中的 `system_prompt`: - -```python -system_prompt = """ -你是一位[角色定义]。 -请用[语言风格]来回答有关[领域]的问题。 -[其他要求...] -""" -``` +## 高级配置 ### 调整图表样式 -在 `matplotlib` 绘图代码中修改: +在 `analysis_functions.py` 中的 matplotlib 绑图代码里修改: ```python # 修改图表大小 @@ -296,48 +545,156 @@ ax.plot(x, y, color='#667eea', linewidth=2) ax.grid(True, alpha=0.3, linestyle='--') ``` -## 📚 参考资料 +### 自定义系统提示词 + +修改 `chatbot.py` 中的 `system_prompt`: + +```python +system_prompt = """ +你是一位精通自动控制原理的专家教授。 +请用清晰、准确、专业的中文来回答问题。 +重要规则: +1. 当需要表达数学公式时,必须使用 LaTeX 格式 +2. 行内公式使用 $公式$ 或 \\(公式\\) +3. 独立公式使用 $$公式$$ 或 \\[公式\\] +""" +``` + +### 自定义工况模板 + +修改 `case_demo_functions.py` 中的 `_profile_points` 函数: + +```python +def _profile_points(profile_name): + if profile_name == "我的自定义工况": + return [ + (0., 1500., 50.), # (时间, 目标转速, 负载转矩) + (10., 3000., 200.), + (30., 2800., 150.), + (50., 1800., 60.) + ] + # ... 其他工况 +``` + +--- + +## 常见问题 + +### Q1: 运行时提示 "psutil 未安装" + +不影响主要功能,仅系统资源监控不可用。忽略此提示或执行: + +```bash +pip install psutil +``` + +### Q2: 算例演示提示 "engine_nn_proxy.pth not found" + +需要先完成**阶段零**的 NN 模型训练(约2~5分钟)。 + +### Q3: GPR 训练失败,提示缺少 botorch/gpytorch + +GPR 训练需要额外依赖。安装方法: + +```bash +pip install botorch gpytorch scikit-learn +``` + +或直接选择 "load" 模式加载已有模型。 + +### Q4: AI 问答返回 "API_KEY 未配置" + +请在 `config.py` 中填入有效的 DeepSeek API 密钥。 + +### Q5: 图表显示中文乱码 + +本平台图表使用英文标签以避免中文显示问题。如需修改,编辑 `analysis_functions.py` 中的 `plt.title()`、`plt.xlabel()` 等。 + +--- + +## 参考资料 ### 经典教材 -- 《自动控制原理》- 胡寿松 -- 《现代控制工程》- Katsuhiko Ogata -- 《反馈控制理论》- John Doyle + +1. 胡寿松.《自动控制原理》(第七版). 科学出版社, 2019. +2. Katsuhiko Ogata. *Modern Control Engineering* (5th Edition). Prentice Hall, 2010. +3. Richard C. Dorf, Robert H. Bishop. *Modern Control Systems* (14th Edition). Pearson, 2021. +4. John Doyle, Bruce Francis, Allen Tannenbaum. *Feedback Control Theory*. Macmillan, 1992. ### 在线资源 + - [python-control 官方文档](https://python-control.readthedocs.io/) - [DeepSeek API 文档](https://platform.deepseek.com/docs) -- [Gradio 官方文档](https://www.gradio.app/docs) +- [Gradio 官方文档](https://gradio.app/docs) +- [PyTorch 文档](https://pytorch.org/docs/) -## 🤝 贡献指南 +--- + +## 贡献指南 欢迎提交 Issue 和 Pull Request! ### 贡献方向 + - 🐛 修复 Bug -- ✨ 添加新功能(如状态空间分析) +- ✨ 添加新功能(如状态空间分析模块) - 📝 改进文档 - 🎨 优化界面设计 - 🧪 添加测试用例 -## 📄 许可证 +### 开发环境设置 + +```bash +# 克隆仓库 +git clone +cd AutoControlCourse + +# 创建开发分支 +git checkout -b feature/your-feature-name + +# 安装开发依赖 +pip install -r requirements.txt +pip install pytest black flake8 + +# 代码格式化 +black . + +# 运行测试 +pytest +``` + +--- + +## 更新日志 + +详细更新日志请参阅 [CHANGELOG.md](CHANGELOG.md)。 + +--- + +## 许可证 本项目采用 MIT 许可证。详见 [LICENSE](LICENSE) 文件。 -## 🙏 致谢 +--- + +## 致谢 - **Gradio**:提供优秀的 Web 界面框架 - **python-control**:强大的控制系统分析库 - **DeepSeek**:高质量的 AI 服务 - 所有贡献者和使用者 -## 📞 联系方式 +--- -- 项目地址:[GitHub Repository URL] -- 问题反馈:[Issues URL] -- 邮箱:[your-email@example.com] +## 联系方式 + +- **项目负责人**:魏鹏飞 +- **电子邮件**:pengfeiwei@nwpu.edu.cn +- **机构**:西北工业大学 +- **项目地址**:https://github.com/your-repo --- -**⭐ 如果这个项目对您有帮助,请给它一个 Star!** +**⭐ 如果这个项目对您的学习有帮助,请给它一个 Star!** 最后更新:2026年4月7日 diff --git a/data_usage/usage_stats.json b/data_usage/usage_stats.json index 9e8f16e..56e565d 100644 --- a/data_usage/usage_stats.json +++ b/data_usage/usage_stats.json @@ -1,4 +1,4 @@ { "total_users": 37, - "last_saved_at": 1775560914.9575145 + "last_saved_at": 1775561044.8460488 } \ No newline at end of file -- 2.54.0