水箱液位pid控制仿真,综合一阶滞后对象+阀门流量特性+不同厂家pid算法
页面截图

网页链接:https://github.com/wy10xxs/pid/blob/main/index.html
页面代码
`
<div class="status-display">
<div class="status-item">
<div>当前液位</div>
<div class="status-value" id="currentLevel">39.9%</div>
</div>
<div class="status-item">
<div>设定值</div>
<div class="status-value" id="currentSetpoint">40.0%</div>
</div>
<div class="status-item">
<div>控制输出</div>
<div class="status-value" id="currentOutput">83.5%</div>
</div>
<div class="status-item">
<div>阀门开度</div>
<div class="status-value" id="currentValve">83.1%</div>
</div>
</div>
</header>
<!-- 图表区域 -->
<section class="chart-section">
<div class="chart-tabs">
<div class="chart-tab active" data-tab="level">响应曲线</div>
<div class="chart-tab" data-tab="flow">流量特性</div>
</div>
<div class="chart-container">
<canvas id="levelChart" style="display: block;"></canvas>
<canvas id="flowCharChart" style="display: none;"></canvas>
</div>
</section>
<!-- 参数控制面板 -->
<div class="tabs-container">
<div class="tab-buttons">
<button class="tab-button active" data-tab="performance">性能参数</button>
<button class="tab-button" data-tab="pid">PID参数</button>
<button class="tab-button" data-tab="tank">水箱参数</button>
<button class="tab-button" data-tab="inValve">进水阀</button>
<button class="tab-button" data-tab="outValve">出水阀</button>
</div>
<!-- 性能参数面板 -->
<div class="tab-content active" id="performance-tab">
<div class="metrics">
<div class="metric">
<div>超调量</div>
<div id="metricOvershoot">3.74 %</div>
</div>
<div class="metric">
<div>稳态误差</div>
<div id="metricSSE">0.102</div>
</div>
<div class="metric">
<div>上升时间</div>
<div id="metricTr">22.90 s</div>
</div>
<div class="metric">
<div>调节时间</div>
<div id="metricTs">217.60 s</div>
</div>
</div>
</div>
<!-- PID参数面板 -->
<div class="tab-content" id="pid-tab">
<div class="field-group">
<div class="field">
<label>PID 算法</label>
<select id="pidMode">
<option value="ideal">理想型 PID</option>
<option value="siemens" selected>西门子增量式</option>
<option value="macs">MACS(带微分滤波)</option>
<option value="ecs">ECS-700(限幅/抗饱和)</option>
</select>
</div>
<div class="field">
<label>Kp</label>
<input id="kp" type="number" step="0.01" value="2.0">
</div>
<div class="field">
<label>Ti (积分 s)</label>
<input id="ti" type="number" step="0.01" value="10">
</div>
<div class="field">
<label>Td (微分 s)</label>
<input id="td" type="number" step="0.01" value="0.5">
</div>
<div class="field">
<label>输出上限 %</label>
<input id="outMax" type="number" step="0.1" value="100">
</div>
<div class="field">
<label>输出下限 %</label>
<input id="outMin" type="number" step="0.1" value="0">
</div>
<div class="field range">
<label>采样周期 s</label>
<input id="samplePeriod" class="slider" type="range" min="0.01" max="1" step="0.01" value="0.1">
<div class="small" id="samplePeriodLabel">0.10 s</div>
</div>
<div class="field range">
<label>仿真速度</label>
<input id="simSpeed" class="slider" type="range" min="0.1" max="5" step="0.1" value="1">
<div class="small" id="simSpeedLabel">1x</div>
</div>
</div>
</div>
<!-- 水箱参数面板 -->
<div class="tab-content" id="tank-tab">
<div class="field-group">
<div class="field">
<label>水箱容积 L</label>
<input id="tankVol" type="number" step="0.1" value="100">
</div>
<div class="field">
<label>初始液位 %</label>
<input id="initLevel" type="number" step="0.1" value="20">
</div>
<div class="field">
<label>目标液位 % (SV)</label>
<input id="targetLevel" type="number" step="0.1" value="60">
</div>
<div class="field">
<label>单位时间步长 (s)</label>
<input id="dt" type="number" step="0.01" value="0.1">
</div>
<div class="field range">
<label>水箱噪声扰动 %</label>
<input id="tankNoise" class="slider" type="range" min="0" max="10" step="0.1" value="1">
<div class="small" id="tankNoiseLabel">1.0%</div>
</div>
</div>
</div>
<!-- 进水阀参数面板 -->
<div class="tab-content" id="inValve-tab">
<div class="field-group">
<div class="field">
<label>最大流量 L/s</label>
<input id="inMaxFlow" type="number" step="0.1" value="5">
</div>
<div class="field">
<label>流量特性</label>
<select id="inChar">
<option value="linear">线性</option>
<option value="equal">等百分比</option>
<option value="parabolic">抛物线型</option>
<option value="quick">快开</option>
</select>
</div>
<div class="field">
<label>响应延迟 s (L)</label>
<input id="inDelay" type="number" step="0.01" value="0.2">
</div>
<div class="field">
<label>时间常数 s (τ)</label>
<input id="inTau" type="number" step="0.01" value="0.5">
</div>
<div class="field">
<label>死区范围 %</label>
<input id="inDeadband" type="number" step="0.1" value="0">
</div>
</div>
</div>
<!-- 出水阀参数面板 -->
<div class="tab-content" id="outValve-tab">
<div class="field-group">
<div class="field">
<label>出水最大流量 L/s</label>
<input id="outMaxFlow" type="number" step="0.1" value="3">
</div>
<div class="field">
<label>流量特性</label>
<select id="outChar">
<option value="linear">线性</option>
<option value="equal">等百分比</option>
<option value="parabolic">抛物线型</option>
<option value="quick">快开</option>
</select>
</div>
<div class="field range">
<label>出水开度 %</label>
<input id="outOpen" class="slider" type="range" min="0" max="100" step="0.1" value="50">
<div class="small" id="outOpenLabel">50%</div>
</div>
<div class="field range">
<label>噪声扰动 %</label>
<input id="outNoise" class="slider" type="range" min="0" max="20" step="0.1" value="5">
<div class="small" id="outNoiseLabel">5%</div>
</div>
<div class="field range">
<label>噪声周期 s</label>
<input id="noisePeriod" class="slider" type="range" min="5" max="50" step="1" value="10">
<div class="small" id="noisePeriodLabel">10s</div>
</div>
</div>
</div>
</div>
<script>
// ================================ 系统常量与工具函数 ================================
/**
* 流量特性计算函数
* @param {string} type - 流量特性类型
* @param {number} openPct - 阀门开度百分比
* @returns {number} 相对流量值 [0,1]
*/
function flowCharacteristic(type, openPct) {
const x = Math.max(0, Math.min(100, openPct)) / 100;
switch(type){
case 'linear':
return x;
case 'equal':
return (Math.pow(10, (x*3)) -1)/(Math.pow(10,3)-1);
case 'parabolic':
return Math.pow(x,2);
case 'quick':
return Math.sqrt(x);
default:
return x;
}
}
/**
* 生成低频正弦波噪声
* @param {number} amplitude - 噪声幅度 (0-1)
* @param {number} period - 噪声周期 (秒)
* @param {number} time - 当前时间
* @returns {number} 噪声值 [-amplitude, amplitude]
*/
function generateLowFreqNoise(amplitude, period, time) {
return amplitude * Math.sin(2 * Math.PI * time / period);
}
// ================================ PID 控制器类定义 ================================
/**
* PID 控制器基类
*/
class PIDBase {
constructor(kp = 1, ti = 10, td = 0) {
this.kp = kp; // 比例系数
this.ti = ti; // 积分时间
this.td = td; // 微分时间
this.lastError = 0; // 上次误差
this.prevError = 0; // 上上次误差
this.integral = 0; // 积分项
this.lastOutput = 0; // 上次输出
this.ts = 0.1; // 采样周期
this.outMin = 0; // 输出下限
this.outMax = 100; // 输出上限
}
reset() {
this.lastError = 0;
this.prevError = 0;
this.integral = 0;
this.lastOutput = 0;
}
setParams(kp, ti, td) {
this.kp = kp;
this.ti = ti;
this.td = td;
}
setSampleTime(ts) {
this.ts = ts;
}
setLimits(min, max) {
this.outMin = min;
this.outMax = max;
}
}
/**
* 理想型 PID 控制器
*/
class PID_Ideal extends PIDBase {
update(setpoint, pv) {
const error = setpoint - pv;
this.integral += error * this.ts;
const derivative = (error - this.lastError) / this.ts;
let output = this.kp * (error + (this.integral / (this.ti || 1e-9)) + this.td * derivative);
// 输出限幅
output = Math.max(this.outMin, Math.min(this.outMax, output));
this.lastError = error;
this.lastOutput = output;
return output;
}
}
/**
* 西门子增量式 PID 控制器
*/
class PID_Siemens extends PIDBase {
update(setpoint, pv) {
const error = setpoint - pv;
// 西门子增量式PID公式:
// Δu(k) = Kp * [(e(k) - e(k-1)) + (Ts/Ti) * e(k) + (Td/Ts) * (e(k) - 2*e(k-1) + e(k-2))]
// u(k) = u(k-1) + Δu(k)
// 计算增量
const deltaP = this.kp * (error - this.lastError);
const deltaI = this.kp * (this.ts / (this.ti || 1e-9)) * error;
const deltaD = this.kp * (this.td / this.ts) * (error - 2 * this.lastError + this.prevError);
const deltaU = deltaP + deltaI + deltaD;
// 计算新输出
let output = this.lastOutput + deltaU;
// 输出限幅
output = Math.max(this.outMin, Math.min(this.outMax, output));
// 更新历史值
this.prevError = this.lastError;
this.lastError = error;
this.lastOutput = output;
return output;
}
}
/**
* MACS PID 控制器(带微分滤波)
*/
class PID_MACS extends PIDBase {
constructor(kp = 1, ti = 10, td = 0) {
super(kp, ti, td);
this.filterCoeff = 0.1; // 微分项滤波系数
this.lastFiltered = 0; // 上次滤波后的微分值
}
update(setpoint, pv) {
const error = setpoint - pv;
this.integral += error * this.ts;
const derivative = (error - this.lastError) / this.ts;
const filteredD = this.filterCoeff * derivative + (1 - this.filterCoeff) * this.lastFiltered;
let output = this.kp * (error + (this.integral / (this.ti || 1e-9)) + this.td * filteredD);
// 输出限幅
output = Math.max(this.outMin, Math.min(this.outMax, output));
this.lastError = error;
this.lastFiltered = filteredD;
this.lastOutput = output;
return output;
}
}
/**
* ECS-700 PID 控制器(带输出限幅和抗饱和)
*/
class PID_ECS extends PIDBase {
update(setpoint, pv) {
const error = setpoint - pv;
this.integral += error * this.ts;
const derivative = (error - this.lastError) / this.ts;
let output = this.kp * (error + (this.integral / (this.ti || 1e-9)) + this.td * derivative);
// 输出限幅和抗积分饱和
if(output > this.outMax) {
output = this.outMax;
this.integral -= error * this.ts; // 积分抗饱和
} else if(output < this.outMin) {
output = this.outMin;
this.integral -= error * this.ts; // 积分抗饱和
}
this.lastError = error;
this.lastOutput = output;
return output;
}
}
// ================================ 阀门动态模型 ================================
/**
* 阀门动态模型:一阶滞后 + 延迟 + 死区
*/
class ValveDynamics {
constructor(tau, delay, deadband) {
this.tau = Math.max(1e-6, tau); // 时间常数
this.delay = Math.max(0, delay); // 延迟时间
this.deadband = deadband / 100; // 死区范围(转换为比例)
this.queue = []; // 延迟队列
this.y = 0; // 当前输出值
}
/**
* 执行一步阀门动态计算
* @param {number} cmd - 控制指令 [0,1]
* @param {number} dt - 时间步长
* @returns {number} 阀门实际开度 [0,1]
*/
step(cmd, dt) {
// 应用死区
if(Math.abs(cmd - this.y) < this.deadband) {
cmd = this.y;
}
// 将当前命令加入队列
this.queue.push(cmd);
// 计算延迟样本数
const samplesDelay = Math.round(this.delay / dt);
// 获取延迟后的命令
let delayed = this.queue.length > samplesDelay ?
this.queue.shift() : this.queue[0];
// 一阶滞后响应
this.y += (delayed - this.y) * (dt / this.tau);
// 限制输出在0-1范围内
this.y = Math.max(0, Math.min(1, this.y));
return this.y;
}
reset() {
this.queue = [];
this.y = 0;
}
}
// ================================ 图表初始化 ================================
// 获取Canvas上下文
const levelCtx = document.getElementById('levelChart').getContext('2d');
const flowCharCtx = document.getElementById('flowCharChart').getContext('2d');
// 通用图表数据集选项
const commonDatasetOptions = {
pointRadius: 0,
tension: 0.35,
borderWidth: 1,
fill: false
};
// 液位响应图表数据
const levelData = {
labels: [],
datasets: [
Object.assign({label: '液位 %', data: []}, commonDatasetOptions, {borderColor: '#0b74de'}),
Object.assign({label: '设定值 %', data: []}, commonDatasetOptions, {borderColor: '#e85d04', borderDash: [6, 4]}),
Object.assign({label: '控制输出 %', data: []}, commonDatasetOptions, {borderColor: '#11999e'}),
Object.assign({label: '阀门开度 %', data: []}, commonDatasetOptions, {borderColor: '#8a2be2'})
]
};
// 液位响应图表配置
const levelChart = new Chart(levelCtx, {
type: 'line',
data: levelData,
options: {
animation: false,
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
font: {
size: 12,
weight: '500'
},
boxWidth: 12,
padding: 8
}
}
},
scales: {
x: {
display: true,
grid: {
color: 'rgba(0,0,0,0.1)'
},
title: {
display: false
},
ticks: {
font: {
size: 11
},
color: '#666'
}
},
y: {
beginAtZero: true,
max: 100,
grid: {
color: 'rgba(0,0,0,0.1)'
},
title: {
display: false
},
ticks: {
font: {
size: 11
},
color: '#666'
}
}
}
}
});
// 流量特性图表数据 - 包含进水阀和出水阀特性
const flowCharData = {
labels: [],
datasets: [
Object.assign({label: '进水阀流量特性', data: []}, commonDatasetOptions, {borderColor: '#6a4c93'}),
Object.assign({label: '出水阀流量特性', data: []}, commonDatasetOptions, {borderColor: '#d9534f'})
]
};
// 流量特性图表配置
const flowCharChart = new Chart(flowCharCtx, {
type: 'line',
data: flowCharData,
options: {
animation: false,
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: true,
position: 'top',
labels: {
font: {
size: 12,
weight: '500'
},
boxWidth: 12,
padding: 8
}
}
},
scales: {
x: {
grid: {
color: 'rgba(0,0,0,0.1)'
},
title: {
display: false
},
ticks: {
font: {
size: 11
},
color: '#666'
}
},
y: {
beginAtZero: true,
max: 1,
grid: {
color: 'rgba(0,0,0,0.1)'
},
title: {
display: false
},
ticks: {
font: {
size: 11
},
color: '#666'
}
}
}
}
});
// ================================ DOM 元素引用 ================================
// 控制按钮
const startBtn = document.getElementById('startBtn');
const pauseBtn = document.getElementById('pauseBtn');
const resetBtn = document.getElementById('resetBtn');
// 滑块和标签
const samplePeriod = document.getElementById('samplePeriod');
const samplePeriodLabel = document.getElementById('samplePeriodLabel');
const simSpeed = document.getElementById('simSpeed');
const simSpeedLabel = document.getElementById('simSpeedLabel');
const outOpen = document.getElementById('outOpen');
const outOpenLabel = document.getElementById('outOpenLabel');
const outNoise = document.getElementById('outNoise');
const outNoiseLabel = document.getElementById('outNoiseLabel');
const noisePeriod = document.getElementById('noisePeriod');
const noisePeriodLabel = document.getElementById('noisePeriodLabel');
const tankNoise = document.getElementById('tankNoise');
const tankNoiseLabel = document.getElementById('tankNoiseLabel');
// 实时状态显示
const currentLevel = document.getElementById('currentLevel');
const currentSetpoint = document.getElementById('currentSetpoint');
const currentOutput = document.getElementById('currentOutput');
const currentValve = document.getElementById('currentValve');
// 性能指标显示
const metricOvershoot = document.getElementById('metricOvershoot');
const metricSSE = document.getElementById('metricSSE');
const metricTr = document.getElementById('metricTr');
const metricTs = document.getElementById('metricTs');
// PID算法选择
const pidMode = document.getElementById('pidMode');
// ================================ 系统参数读取 ================================
/**
* 从界面读取所有参数
* @returns {Object} 参数对象
*/
function readParams() {
return {
// 水箱参数
tankVol: parseFloat(document.getElementById('tankVol').value),
initLevel: parseFloat(document.getElementById('initLevel').value),
targetLevel: parseFloat(document.getElementById('targetLevel').value),
dt: parseFloat(document.getElementById('dt').value),
tankNoise: parseFloat(tankNoise.value),
// 进水阀参数
inMaxFlow: parseFloat(document.getElementById('inMaxFlow').value),
inChar: document.getElementById('inChar').value,
inDelay: parseFloat(document.getElementById('inDelay').value),
inTau: parseFloat(document.getElementById('inTau').value),
inDeadband: parseFloat(document.getElementById('inDeadband').value),
// 出水阀参数
outMaxFlow: parseFloat(document.getElementById('outMaxFlow').value),
outChar: document.getElementById('outChar').value,
outOpen: parseFloat(outOpen.value),
outNoise: parseFloat(outNoise.value),
noisePeriod: parseFloat(noisePeriod.value),
// PID 参数
pidMode: document.getElementById('pidMode').value,
kp: parseFloat(document.getElementById('kp').value),
ti: parseFloat(document.getElementById('ti').value),
td: parseFloat(document.getElementById('td').value),
outMax: parseFloat(document.getElementById('outMax').value),
outMin: parseFloat(document.getElementById('outMin').value),
// 仿真参数
samplePeriod: parseFloat(samplePeriod.value),
simSpeed: parseFloat(simSpeed.value)
};
}
// ================================ PID 实例创建 ================================
/**
* 根据模式创建 PID 实例
* @param {string} mode - PID 模式
* @param {number} kp - 比例系数
* @param {number} ti - 积分时间
* @param {number} td - 微分时间
* @param {number} outMin - 输出下限
* @param {number} outMax - 输出上限
* @param {number} ts - 采样时间
* @returns {PIDBase} PID 控制器实例
*/
function createPIDByMode(mode, kp, ti, td, outMin, outMax, ts) {
let pid;
if(mode === 'ideal') {
pid = new PID_Ideal(kp, ti, td);
} else if(mode === 'siemens') {
pid = new PID_Siemens(kp, ti, td);
} else if(mode === 'macs') {
pid = new PID_MACS(kp, ti, td);
} else {
pid = new PID_ECS(kp, ti, td);
}
pid.setSampleTime(ts);
pid.setLimits(outMin, outMax);
return pid;
}
// ================================ 仿真状态管理 ================================
// 仿真状态变量
let simTimer = null; // 仿真定时器
let simRunning = false; // 仿真运行状态
let state = {}; // 仿真状态对象
/**
* 重置仿真
*/
function resetSimulation() {
const p = readParams();
// 停止当前仿真
if(simTimer) {
clearInterval(simTimer);
simTimer = null;
}
simRunning = false;
// 初始化仿真状态
state = {
t: 0, // 当前时间
level: p.initLevel, // 当前液位
params: p, // 当前参数
inValveCmd: 0, // 进水阀控制指令
inValveDyn: new ValveDynamics(p.inTau, p.inDelay, p.inDeadband), // 进水阀动态模型
outValveNoise: 0, // 出水阀噪声
tankNoiseValue: 0, // 水箱噪声值
pid: createPIDByMode(p.pidMode, p.kp, p.ti, p.td, p.outMin, p.outMax, p.samplePeriod), // PID控制器
// 历史数据
history: {
time: [],
level: [], // 液位历史
setpoint: [], // 设定值历史
output: [], // 控制输出历史
valve: [] // 阀门开度历史
}
};
// 清空图表数据
levelData.labels.length = 0;
levelData.datasets.forEach(ds => ds.data.length = 0);
// 更新流量特性图表
updateFlowCharChart();
// 重置性能指标显示
resetMetricsDisplay();
// 更新状态显示
updateStatusDisplay();
// 更新图表
levelChart.update();
flowCharChart.update();
}
/**
* 重置性能指标显示
*/
function resetMetricsDisplay() {
metricOvershoot.textContent = '-';
metricSSE.textContent = '-';
metricTr.textContent = '-';
metricTs.textContent = '-';
}
/**
* 应用参数变化(不清空历史)
*/
function applyParamChanges() {
const p = readParams();
// 如果状态尚未初始化,则重置仿真
if(!state.params) {
state.params = p;
return;
}
// 更新参数
state.params.tankVol = p.tankVol;
state.params.targetLevel = p.targetLevel;
state.params.tankNoise = p.tankNoise;
state.params.inMaxFlow = p.inMaxFlow;
state.params.inChar = p.inChar;
state.params.inDelay = p.inDelay;
state.params.inTau = p.inTau;
state.params.inDeadband = p.inDeadband;
state.params.outMaxFlow = p.outMaxFlow;
state.params.outChar = p.outChar;
state.params.outOpen = p.outOpen;
state.params.outNoise = p.outNoise;
state.params.noisePeriod = p.noisePeriod;
state.params.samplePeriod = p.samplePeriod;
state.params.simSpeed = p.simSpeed;
// 检查是否需要更换PID算法
if(state.params.pidMode !== p.pidMode) {
state.params.pidMode = p.pidMode;
state.pid = createPIDByMode(p.pidMode, p.kp, p.ti, p.td, p.outMin, p.outMax, p.samplePeriod);
state.pid.setSampleTime(p.samplePeriod);
state.pid.reset(); // 重置积分项
// 重置性能指标
resetMetricsDisplay();
}
// 更新PID参数
state.params.kp = p.kp;
state.params.ti = p.ti;
state.params.td = p.td;
if(state.pid) {
state.pid.setParams(p.kp, p.ti, p.td);
state.pid.setSampleTime(p.samplePeriod);
state.pid.setLimits(p.outMin, p.outMax);
}
// 更新阀门动态参数
if(state.inValveDyn) {
state.inValveDyn.tau = p.inTau;
state.inValveDyn.delay = p.inDelay;
state.inValveDyn.deadband = p.inDeadband / 100;
}
// 更新流量特性图表
updateFlowCharChart();
// 如果采样周期改变,重启仿真定时器
if(simRunning) {
pauseSim();
startSim();
}
}
/**
* 更新流量特性图表
*/
function updateFlowCharChart() {
const p = readParams();
// 清空现有数据
flowCharData.labels.length = 0;
flowCharData.datasets[0].data.length = 0;
flowCharData.datasets[1].data.length = 0;
// 生成新的流量特性数据
for(let i = 0; i <= 100; i += 1) {
flowCharData.labels.push(i);
flowCharData.datasets[0].data.push(flowCharacteristic(p.inChar, i));
flowCharData.datasets[1].data.push(flowCharacteristic(p.outChar, i));
}
// 更新图表
flowCharChart.update();
}
/**
* 计算出水流量(含噪声)
* @param {number} levelPct - 当前液位百分比
* @param {Object} params - 参数对象
* @returns {number} 出水流量
*/
function computeOutflow(levelPct, params) {
// 计算基础流量
const rel = flowCharacteristic(params.outChar, Math.max(0, Math.min(100, params.outOpen)));
let baseOutflow = params.outMaxFlow * rel;
// 添加低频正弦波噪声
if(params.outNoise > 0) {
// 使用低频正弦波噪声,模拟真实系统中的压力波动
const noiseFactor = generateLowFreqNoise(params.outNoise / 100, params.noisePeriod, state.t);
state.outValveNoise = noiseFactor;
// 应用噪声到流量
let outflow = baseOutflow * (1 + noiseFactor);
// 确保出水流量非负
return Math.max(0, outflow);
} else {
state.outValveNoise = 0;
return baseOutflow;
}
}
/**
* 计算性能指标
* @param {Array} history - 液位历史数据
* @param {number} target - 目标液位
* @param {number} samplePeriod - 采样周期
* @returns {Object} 性能指标对象
*/
function calcMetrics(history, target, samplePeriod) {
if(!history || history.length < 2) {
return {};
}
const lvl = history;
const max = Math.max(...lvl);
const overshoot = Math.max(0, (max - target) / target * 100);
const ss = lvl[lvl.length - 1];
const sse = Math.abs(target - ss);
// 计算上升时间(从10%到90%)
let t10 = null, t90 = null;
for(let i = 0; i < lvl.length; i++) {
const v = lvl[i];
if(t10 === null && v >= target * 0.1) {
t10 = i;
}
if(t90 === null && v >= target * 0.9) {
t90 = i;
}
}
const tr = (t10 !== null && t90 !== null) ? ((t90 - t10) * samplePeriod).toFixed(2) + ' s' : '-';
// 计算调节时间(进入±2%误差带)
let ts = null;
const tol = target * 0.02;
for(let i = lvl.length - 1; i >= 0; i--) {
if(Math.abs(lvl[i] - target) > tol) {
ts = i + 1;
break;
}
}
const tsStr = ts === null ? '-' : ((lvl.length - ts) * samplePeriod).toFixed(2) + ' s';
return {
overshoot: overshoot.toFixed(2) + ' %',
sse: sse.toFixed(3),
tr: tr,
ts: tsStr
};
}
/**
* 更新性能指标显示
*/
function updateMetricsDisplay() {
const m = calcMetrics(state.history.level || [], state.params.targetLevel, state.params.samplePeriod);
document.getElementById('metricOvershoot').textContent = m.overshoot || '-';
document.getElementById('metricSSE').textContent = m.sse || '-';
document.getElementById('metricTr').textContent = m.tr || '-';
document.getElementById('metricTs').textContent = m.ts || '-';
}
/**
* 更新状态显示
*/
function updateStatusDisplay() {
currentLevel.textContent = state.level.toFixed(1) + '%';
currentSetpoint.textContent = state.params.targetLevel.toFixed(1) + '%';
currentOutput.textContent = (state.inValveCmd * 100).toFixed(1) + '%';
currentValve.textContent = (state.history.valve.length > 0 ?
state.history.valve[state.history.valve.length - 1].toFixed(1) : '0.0') + '%';
}
/**
* 执行一步仿真
*/
function simStep() {
const p = state.params;
const dt = p.samplePeriod;
// PID 控制计算
const pidOut = state.pid.update(p.targetLevel, state.level);
// 限制 PID 输出在 0-100% 范围内
const uPct = Math.max(p.outMin, Math.min(p.outMax, pidOut));
// 进水阀控制(转换为0-1范围)
state.inValveCmd = uPct / 100;
// 进水阀动态响应
const actualOpening = state.inValveDyn.step(state.inValveCmd, dt);
// 计算进水流量
const inFlow = p.inMaxFlow * flowCharacteristic(p.inChar, actualOpening * 100);
// 计算出水流量(含噪声)
const outFlow = computeOutflow(state.level, p);
// 计算液位变化
const V = p.tankVol;
const dV = (inFlow - outFlow) * dt;
const levelLiters = (state.level / 100) * V + dV;
// 添加水箱噪声扰动
if(p.tankNoise > 0) {
const noiseFactor = (Math.random() - 0.5) * 2 * (p.tankNoise / 100);
state.tankNoiseValue = noiseFactor;
const noiseVolume = V * noiseFactor/10;
const levelLitersWithNoise = levelLiters + noiseVolume;
state.level = Math.max(0, Math.min(100, (levelLitersWithNoise / V) * 100));
} else {
state.tankNoiseValue = 0;
state.level = Math.max(0, Math.min(100, (levelLiters / V) * 100));
}
// 更新时间
state.t += dt;
// 记录历史数据
state.history.time.push(state.t);
state.history.level.push(state.level);
state.history.setpoint.push(p.targetLevel);
state.history.output.push(uPct);
state.history.valve.push(actualOpening * 100);
// 更新图表数据(限制数据点数量)
const maxPoints = 1200;
if(levelData.labels.length > maxPoints) {
levelData.labels.shift();
levelData.datasets.forEach(ds => ds.data.shift());
}
levelData.labels.push(state.t.toFixed(2));
levelData.datasets[0].data.push(state.level);
levelData.datasets[1].data.push(p.targetLevel);
levelData.datasets[2].data.push(uPct);
levelData.datasets[3].data.push(actualOpening * 100);
// 更新图表
levelChart.update();
// 更新性能指标
updateMetricsDisplay();
// 更新状态显示
updateStatusDisplay();
}
/**
* 开始仿真
*/
function startSim() {
if(simRunning) return;
simRunning = true;
const p = readParams();
// 更新状态参数
state.params = Object.assign(state.params || {}, p);
// 确保PID控制器存在
if(!state.pid) {
state.pid = createPIDByMode(p.pidMode, p.kp, p.ti, p.td, p.outMin, p.outMax, p.samplePeriod);
}
// 更新PID参数
state.pid.setParams(p.kp, p.ti, p.td);
state.pid.setSampleTime(p.samplePeriod);
state.pid.setLimits(p.outMin, p.outMax);
// 确保进水阀动态模型存在
if(!state.inValveDyn) {
state.inValveDyn = new ValveDynamics(p.inTau, p.inDelay, p.inDeadband);
} else {
state.inValveDyn.tau = p.inTau;
state.inValveDyn.delay = p.inDelay;
state.inValveDyn.deadband = p.inDeadband / 100;
}
// 设置仿真定时器
const interval = p.samplePeriod * 1000 / p.simSpeed;
simTimer = setInterval(() => {
simStep();
}, Math.max(4, interval));
}
/**
* 暂停仿真
*/
function pauseSim() {
if(simTimer) {
clearInterval(simTimer);
}
simTimer = null;
simRunning = false;
}
// ================================ 事件绑定 ================================
// 图表选项卡切换逻辑
document.querySelectorAll('.chart-tab').forEach(tab => {
tab.addEventListener('click', function() {
// 移除所有活动标签
document.querySelectorAll('.chart-tab').forEach(t => t.classList.remove('active'));
// 添加当前活动标签
this.classList.add('active');
// 切换图表显示
const tabType = this.getAttribute('data-tab');
if (tabType === 'level') {
document.getElementById('levelChart').style.display = 'block';
document.getElementById('flowCharChart').style.display = 'none';
} else {
document.getElementById('levelChart').style.display = 'none';
document.getElementById('flowCharChart').style.display = 'block';
}
});
});
// 参数选项卡切换逻辑
document.querySelectorAll('.tab-button').forEach(button => {
button.addEventListener('click', function() {
// 移除所有活动标签
document.querySelectorAll('.tab-button').forEach(btn => btn.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
// 添加当前活动标签
this.classList.add('active');
// 显示对应的内容
const tabId = this.getAttribute('data-tab');
document.getElementById(`${tabId}-tab`).classList.add('active');
});
});
// 滑块事件处理
const sliders = [
{ id: 'samplePeriod', label: 'samplePeriodLabel', format: val => parseFloat(val).toFixed(2) + ' s' },
{ id: 'simSpeed', label: 'simSpeedLabel', format: val => val + 'x' },
{ id: 'outOpen', label: 'outOpenLabel', format: val => val + '%' },
{ id: 'outNoise', label: 'outNoiseLabel', format: val => val + '%' },
{ id: 'noisePeriod', label: 'noisePeriodLabel', format: val => val + 's' },
{ id: 'tankNoise', label: 'tankNoiseLabel', format: val => val + '%' }
];
sliders.forEach(slider => {
const input = document.getElementById(slider.id);
const label = document.getElementById(slider.label);
if (input && label) {
// 初始化标签
label.textContent = slider.format(input.value);
// 更新事件
input.addEventListener('input', () => {
label.textContent = slider.format(input.value);
applyParamChanges();
});
}
});
// 控制按钮事件
startBtn.addEventListener('click', () => {
if(!state.params) {
resetSimulation();
}
startSim();
});
pauseBtn.addEventListener('click', () => {
pauseSim();
});
resetBtn.addEventListener('click', () => {
resetSimulation();
});
// 参数变化事件
const paramIds = [
'inChar', 'inMaxFlow', 'inTau', 'inDelay', 'inDeadband',
'outMaxFlow', 'outChar', 'tankVol', 'initLevel', 'targetLevel', 'tankNoise',
'kp', 'ti', 'td', 'outMax', 'outMin', 'pidMode', 'dt'
];
paramIds.forEach(id => {
document.getElementById(id).addEventListener('change', () => {
const p = readParams();
// 如果状态尚未初始化,则重置仿真
if(!state.params) {
resetSimulation();
return;
}
// 特殊处理:如果初始液位改变,立即更新当前液位
if(id === 'initLevel') {
state.level = p.initLevel;
}
// 如果PID算法改变,重置性能指标
if(id === 'pidMode') {
resetMetricsDisplay();
}
applyParamChanges();
});
});
// 初始化
resetSimulation();
updateFlowCharChart();
</script>
浙公网安备 33010602011771号