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

页面截图

Snipaste_2025-11-10_15-51-19

网页链接:https://github.com/wy10xxs/pid/blob/main/index.html

页面代码
`

水箱液位 PID 控制仿真系统
水箱液位 PID 仿真
  <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>
posted @ 2025-11-10 15:08  谷雨清明  阅读(1)  评论(0)    收藏  举报