独立渲染引擎就绪引擎就绪
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PLC模糊逻辑控制系统 - IFR原理可视化</title>
<style>
:root {
--bg-color: #050914;
--grid-color: rgba(0, 240, 255, 0.05);
--text-main: #a3b8cc;
--text-muted: #536d88;
--accent-cyan: #00f0ff;
--accent-pink: #ff0055;
--accent-green: #00ffaa;
--accent-amber: #ffaa00;
--panel-bg: rgba(10, 15, 30, 0.7);
--border-glow: 0 0 10px rgba(0, 240, 255, 0.2), inset 0 0 15px rgba(0, 240, 255, 0.1);
}
body, html {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
background-color: var(--bg-color);
background-image:
linear-gradient(var(--grid-color) 1px, transparent 1px),
linear-gradient(90deg, var(--grid-color) 1px, transparent 1px);
background-size: 30px 30px;
font-family: 'SF Pro Display', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
color: var(--text-main);
}
.dashboard-container {
position: relative;
width: 100%;
max-width: 1400px;
height: 100vh;
max-height: 900px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
}
/* 顶部边缘文字 - 严格遵守约束,绝不遮挡主体 */
.header-info {
position: absolute;
top: 20px;
left: 30px;
right: 30px;
display: flex;
justify-content: space-between;
align-items: flex-start;
font-size: 12px;
letter-spacing: 1px;
z-index: 10;
pointer-events: none;
}
.header-info .title-block h1 {
margin: 0 0 4px 0;
font-size: 16px;
color: var(--accent-cyan);
text-transform: uppercase;
font-weight: 600;
text-shadow: 0 0 8px rgba(0, 240, 255, 0.4);
}
.header-info .desc {
max-width: 400px;
color: var(--text-muted);
line-height: 1.4;
}
.header-info .ifr-badge {
text-align: right;
max-width: 450px;
}
.ifr-badge .highlight {
color: var(--accent-green);
font-weight: 500;
}
/* 底部边缘交互区 */
.footer-controls {
position: absolute;
bottom: 20px;
left: 30px;
display: flex;
gap: 30px;
z-index: 10;
background: rgba(0,0,0,0.4);
padding: 10px 20px;
border-radius: 8px;
border: 1px solid rgba(0, 240, 255, 0.2);
backdrop-filter: blur(4px);
}
.control-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.control-group label {
font-size: 11px;
color: var(--accent-cyan);
display: flex;
justify-content: space-between;
}
input[type=range] {
-webkit-appearance: none;
width: 150px;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 12px;
width: 12px;
border-radius: 50%;
background: var(--accent-cyan);
cursor: pointer;
margin-top: -5px;
box-shadow: 0 0 10px var(--accent-cyan);
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%;
height: 2px;
cursor: pointer;
background: rgba(255,255,255,0.2);
}
#slider-dedt::-webkit-slider-thumb { background: var(--accent-pink); box-shadow: 0 0 10px var(--accent-pink); }
.mode-indicator {
position: absolute;
bottom: 30px;
right: 30px;
font-size: 12px;
color: var(--accent-amber);
display: flex;
align-items: center;
gap: 8px;
opacity: 0.8;
}
.mode-dot {
width: 8px; height: 8px; border-radius: 50%;
background: var(--accent-amber);
box-shadow: 0 0 8px var(--accent-amber);
animation: pulse 1s infinite alternate;
}
/* 核心 SVG 容器 */
svg {
width: 100%;
height: 100%;
max-width: 1200px;
max-height: 750px;
filter: drop-shadow(0 0 20px rgba(0,0,0,0.5));
}
/* SVG 内部样式 */
.neon-path-cyan { stroke: var(--accent-cyan); fill: none; stroke-linecap: round; }
.neon-path-pink { stroke: var(--accent-pink); fill: none; stroke-linecap: round; }
.neon-path-green { stroke: var(--accent-green); fill: none; stroke-linecap: round; }
.data-flow-line {
stroke-dasharray: 4 8;
animation: flow 1s linear infinite;
}
@keyframes flow {
to { stroke-dashoffset: -12; }
}
@keyframes pulse {
from { opacity: 0.4; transform: scale(0.8); }
to { opacity: 1; transform: scale(1.2); }
}
.glow-text {
font-family: 'Courier New', Courier, monospace;
font-weight: bold;
}
</style>
</head>
<body>
<div class="dashboard-container">
<!-- 边缘文字说明:绝不遮挡中心 -->
<div class="header-info">
<div class="title-block">
<h1>PLC Fuzzy Control Logic</h1>
<div class="desc">自适应控制系统原理分析:基于梯形隶属度与重心去模糊化</div>
</div>
<div class="ifr-badge">
<span class="highlight">IFR (最终理想解) 状态:</span><br>
系统在不增加硬件复杂度的前提下,巧妙利用模糊算法融合<br>
<span style="color:var(--accent-cyan)">[液位偏差 e]</span> 与 <span style="color:var(--accent-pink)">[变化率 de/dt]</span>。<br>
能在偏差较小但突发剧变时<strong style="color:#fff">提前预判动作</strong>,<br>
输出连续平滑的调节量,彻底消除传统阈值开关的滞后与震荡。
</div>
</div>
<!-- 底部交互控制 -->
<div class="footer-controls">
<div class="control-group">
<label for="slider-e"><span>偏差 (e)</span> <span id="val-e">0.0</span></label>
<input type="range" id="slider-e" min="-100" max="100" value="0">
</div>
<div class="control-group">
<label style="color:var(--accent-pink)" for="slider-dedt"><span>变化率 (de/dt)</span> <span id="val-dedt">0.0</span></label>
<input type="range" id="slider-dedt" min="-100" max="100" value="0">
</div>
</div>
<div class="mode-indicator">
<div class="mode-dot" id="mode-dot"></div>
<span id="mode-text">仿真自动运行中 (触控滑块接管)</span>
</div>
<!-- 核心动画区域:占用最大空间 -->
<svg viewBox="0 0 1200 750" preserveAspectRatio="xMidYMid meet" xmlns="http://www.w3.org/2000/svg">
<defs>
<!-- 滤镜定义 -->
<filter id="glow-cyan" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="glow-pink" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="5" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<filter id="glow-green" x="-20%" y="-20%" width="140%" height="140%">
<feGaussianBlur stdDeviation="4" result="blur" />
<feMerge>
<feMergeNode in="blur" />
<feMergeNode in="SourceGraphic" />
</feMerge>
</filter>
<!-- 渐变 -->
<linearGradient id="tank-grad" x1="0" y1="1" x2="0" y2="0">
<stop offset="0%" stop-color="rgba(0, 240, 255, 0.8)"/>
<stop offset="100%" stop-color="rgba(0, 240, 255, 0.1)"/>
</linearGradient>
<marker id="arrow-cyan" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#00f0ff" />
</marker>
<marker id="arrow-pink" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#ff0055" />
</marker>
<marker id="arrow-green" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#00ffaa" />
</marker>
</defs>
<!-- 背景框架 -->
<rect x="50" y="50" width="1100" height="650" rx="16" fill="rgba(10, 15, 30, 0.4)" stroke="rgba(255,255,255,0.05)" stroke-width="2"/>
<text x="600" y="75" fill="#3a4b5c" font-size="14" text-anchor="middle" font-weight="600" letter-spacing="4">VIRTUAL PLC CORE</text>
<!-- ================= 左侧:传感器输入区 ================= -->
<g transform="translate(80, 150)">
<!-- 液位传感器 e -->
<rect x="0" y="0" width="80" height="200" rx="4" fill="rgba(0,0,0,0.5)" stroke="#00f0ff" stroke-width="1" stroke-opacity="0.3"/>
<!-- 目标基准线 -->
<line x1="-10" y1="100" x2="90" y2="100" stroke="#fff" stroke-width="1" stroke-dasharray="4 2"/>
<text x="-15" y="104" fill="#fff" font-size="10" text-anchor="end">SP</text>
<!-- 动态液位 -->
<rect id="viz-level" x="2" y="100" width="76" height="100" fill="url(#tank-grad)"/>
<!-- 游标 -->
<polygon id="viz-e-cursor" points="80,100 90,95 90,105" fill="#00f0ff" filter="url(#glow-cyan)"/>
<text x="40" y="-15" fill="#00f0ff" font-size="14" text-anchor="middle" font-weight="bold">液位偏差 (e)</text>
<!-- 变化率 de/dt -->
<g transform="translate(0, 280)">
<rect x="0" y="0" width="80" height="120" rx="4" fill="rgba(0,0,0,0.5)" stroke="#ff0055" stroke-width="1" stroke-opacity="0.3"/>
<line x1="0" y1="60" x2="80" y2="60" stroke="#333" stroke-width="1"/>
<!-- 动态变化率游标 -->
<polygon id="viz-dedt-cursor" points="80,60 90,55 90,65" fill="#ff0055" filter="url(#glow-pink)"/>
<!-- 波形示意 -->
<path id="viz-wave" d="M 0 60 Q 20 60, 40 60 T 80 60" fill="none" stroke="#ff0055" stroke-width="2" filter="url(#glow-pink)"/>
<text x="40" y="-15" fill="#ff0055" font-size="14" text-anchor="middle" font-weight="bold">变化率 (de/dt)</text>
</g>
</g>
<!-- ================= 数据流连线 ================= -->
<path d="M 170 250 L 250 250 L 250 180 L 320 180" class="neon-path-cyan data-flow-line" stroke-width="2" marker-end="url(#arrow-cyan)"/>
<path d="M 170 490 L 250 490 L 250 360 L 320 360" class="neon-path-pink data-flow-line" stroke-width="2" marker-end="url(#arrow-pink)"/>
<path d="M 880 340 L 960 340" class="neon-path-green data-flow-line" stroke-width="2" marker-end="url(#arrow-green)"/>
<!-- ================= 中部:PLC 模糊逻辑处理核心 ================= -->
<g transform="translate(320, 100)">
<!-- 核心外框 -->
<rect x="0" y="0" width="560" height="520" rx="8" fill="rgba(20, 30, 50, 0.4)" stroke="rgba(255,255,255,0.1)"/>
<!-- 1. Fuzzification e -->
<g transform="translate(20, 20)">
<text x="0" y="15" fill="#a3b8cc" font-size="12">1. 模糊化 (Fuzzification) - 输入 e</text>
<rect x="0" y="30" width="520" height="100" fill="#000" stroke="#333"/>
<!-- 7段隶属度函数基底图 -->
<g id="mf-e-base" stroke="#00f0ff" stroke-width="1" fill="none" opacity="0.3">
<path d="M0,30 L65,30 L130,130 L0,130 Z"/> <!-- NB -->
<path d="M65,130 L130,30 L195,130 Z"/> <!-- NM -->
<path d="M130,130 L195,30 L260,130 Z"/> <!-- NS -->
<path d="M195,130 L260,30 L325,130 Z"/> <!-- Z -->
<path d="M260,130 L325,30 L390,130 Z"/> <!-- PS -->
<path d="M325,130 L390,30 L455,130 Z"/> <!-- PM -->
<path d="M390,130 L455,30 L520,30 L520,130 Z"/> <!-- PB -->
</g>
<!-- 动态高亮多边形 (由JS控制透明度) -->
<g id="mf-e-active" fill="#00f0ff">
<path id="ae-0" d="M0,30 L65,30 L130,130 L0,130 Z" opacity="0"/>
<path id="ae-1" d="M65,130 L130,30 L195,130 Z" opacity="0"/>
<path id="ae-2" d="M130,130 L195,30 L260,130 Z" opacity="0"/>
<path id="ae-3" d="M195,130 L260,30 L325,130 Z" opacity="0"/>
<path id="ae-4" d="M260,130 L325,30 L390,130 Z" opacity="0"/>
<path id="ae-5" d="M325,130 L390,30 L455,130 Z" opacity="0"/>
<path id="ae-6" d="M390,130 L455,30 L520,30 L520,130 Z" opacity="0"/>
</g>
<!-- 动态输入游标线 -->
<line id="line-e" x1="260" y1="30" x2="260" y2="130" stroke="#fff" stroke-width="2" filter="url(#glow-cyan)"/>
<!-- 标签 -->
<text x="260" y="145" fill="#666" font-size="10" text-anchor="middle">NB NM NS Z PS PM PB</text>
</g>
<!-- 2. Fuzzification de/dt -->
<g transform="translate(20, 180)">
<text x="0" y="15" fill="#a3b8cc" font-size="12">1. 模糊化 (Fuzzification) - 输入 de/dt</text>
<rect x="0" y="30" width="520" height="100" fill="#000" stroke="#333"/>
<!-- 7段隶属度函数基底图 -->
<g id="mf-dedt-base" stroke="#ff0055" stroke-width="1" fill="none" opacity="0.3">
<path d="M0,30 L65,30 L130,130 L0,130 Z"/>
<path d="M65,130 L130,30 L195,130 Z"/>
<path d="M130,130 L195,30 L260,130 Z"/>
<path d="M195,130 L260,30 L325,130 Z"/>
<path d="M260,130 L325,30 L390,130 Z"/>
<path d="M325,130 L390,30 L455,130 Z"/>
<path d="M390,130 L455,30 L520,30 L520,130 Z"/>
</g>
<g id="mf-dedt-active" fill="#ff0055">
<path id="ad-0" d="M0,30 L65,30 L130,130 L0,130 Z" opacity="0"/>
<path id="ad-1" d="M65,130 L130,30 L195,130 Z" opacity="0"/>
<path id="ad-2" d="M130,130 L195,30 L260,130 Z" opacity="0"/>
<path id="ad-3" d="M195,130 L260,30 L325,130 Z" opacity="0"/>
<path id="ad-4" d="M260,130 L325,30 L390,130 Z" opacity="0"/>
<path id="ad-5" d="M325,130 L390,30 L455,130 Z" opacity="0"/>
<path id="ad-6" d="M390,130 L455,30 L520,30 L520,130 Z" opacity="0"/>
</g>
<line id="line-dedt" x1="260" y1="30" x2="260" y2="130" stroke="#fff" stroke-width="2" filter="url(#glow-pink)"/>
<text x="260" y="145" fill="#666" font-size="10" text-anchor="middle">NB NM NS Z PS PM PB</text>
</g>
<!-- 3. Defuzzification (重心法) -->
<g transform="translate(20, 350)">
<text x="0" y="15" fill="#a3b8cc" font-size="12">2. 模糊推理 & 3. 去模糊化 (Centroid Defuzzification) - 输出 U</text>
<rect x="0" y="30" width="520" height="120" fill="#000" stroke="#333"/>
<!-- 输出隶属度基底 -->
<g id="mf-out-base" stroke="#00ffaa" stroke-width="1" fill="none" opacity="0.2" stroke-dasharray="2 2">
<path d="M0,30 L65,30 L130,150 L0,150 Z"/>
<path d="M65,150 L130,30 L195,150 Z"/>
<path d="M130,150 L195,30 L260,150 Z"/>
<path d="M195,150 L260,30 L325,150 Z"/>
<path d="M260,150 L325,30 L390,150 Z"/>
<path d="M325,150 L390,30 L455,150 Z"/>
<path d="M390,150 L455,30 L520,30 L520,150 Z"/>
</g>
<!-- 动态聚合多边形 (表示规则触发的面积) -->
<path id="defuzz-area" d="" fill="rgba(0, 255, 170, 0.3)" stroke="#00ffaa" stroke-width="2"/>
<!-- 重心线 -->
<g id="centroid-group" transform="translate(260, 0)">
<line x1="0" y1="30" x2="0" y2="150" stroke="#fff" stroke-width="3" filter="url(#glow-green)"/>
<polygon points="-6,30 6,30 0,38" fill="#fff"/>
<text id="txt-out-val" x="0" y="20" fill="#00ffaa" font-size="14" font-weight="bold" text-anchor="middle" class="glow-text">0.0%</text>
</g>
<text x="260" y="165" fill="#666" font-size="10" text-anchor="middle">100% 关 <------> 100% 开</text>
</g>
</g>
<!-- ================= 右侧:执行器输出区 ================= -->
<g transform="translate(960, 250)">
<!-- 管道基座 -->
<path d="M 0 70 L 120 70 L 120 110 L 0 110 Z" fill="#222" stroke="#444" stroke-width="2"/>
<path d="M 30 75 L 90 75 L 90 105 L 30 105 Z" fill="#111"/>
<!-- 调节阀门体 -->
<circle cx="60" cy="90" r="30" fill="#333" stroke="#00ffaa" stroke-width="2"/>
<!-- 阀芯旋转动画组 -->
<g id="valve-core" transform="rotate(0, 60, 90)">
<circle cx="60" cy="90" r="20" fill="#1a1a1a" stroke="#fff" stroke-width="1"/>
<path d="M 40 90 L 80 90 M 60 70 L 60 110" stroke="#00ffaa" stroke-width="4" filter="url(#glow-green)"/>
</g>
<text x="60" y="40" fill="#00ffaa" font-size="14" text-anchor="middle" font-weight="bold">连续调节阀 (U)</text>
<!-- 流量指示特效 -->
<path id="flow-effect" d="M 10 90 Q 60 90 110 90" stroke="rgba(0, 255, 170, 0.5)" stroke-width="10" stroke-dasharray="10 5" fill="none" opacity="0">
<animate attributeName="stroke-dashoffset" from="15" to="0" dur="0.5s" repeatCount="indefinite" />
</path>
</g>
</svg>
</div>
<script>
/**
* 模糊逻辑控制器核心仿真
*/
// 7个语言变量集合的中心点 (对应 -100 到 100 映射到像素坐标)
const SETS = [-100, -66.6, -33.3, 0, 33.3, 66.6, 100];
const NAMES = ['NB', 'NM', 'NS', 'Z', 'PS', 'PM', 'PB'];
// 梯形/三角形隶属度计算
function getMemberships(val, min = -100, max = 100) {
const range = max - min;
const step = range / 6;
let mems = [0,0,0,0,0,0,0];
for(let i=0; i<7; i++) {
let c = min + i * step;
if(i === 0) { // 左侧梯形
if(val <= c) mems[i] = 1;
else if(val < c + step) mems[i] = 1 - (val - c)/step;
} else if (i === 6) { // 右侧梯形
if(val >= c) mems[i] = 1;
else if(val > c - step) mems[i] = 1 - (c - val)/step;
} else { // 中间三角形
if(val >= c - step && val <= c) mems[i] = (val - (c - step))/step;
else if(val > c && val <= c + step) mems[i] = 1 - (val - c)/step;
}
}
return mems;
}
// 模糊规则矩阵 (7x7) - 工程经验设定
// 横轴: dedt, 纵轴: e
// 核心逻辑: 当e较小(Z)但dedt很大(PB)时,输出也要偏大以抗拒变化,体现提前响应
const ruleMatrix = [
[0, 0, 0, 0, 1, 2, 3], // e = NB
[0, 0, 1, 1, 2, 3, 4], // e = NM
[0, 1, 1, 2, 3, 4, 5], // e = NS
[0, 1, 2, 3, 4, 5, 6], // e = Z
[1, 2, 3, 4, 5, 5, 6], // e = PS
[2, 3, 4, 5, 5, 6, 6], // e = PM
[3, 4, 5, 6, 6, 6, 6] // e = PB
];
// DOM 元素缓存
const els = {
e_cursor: document.getElementById('viz-e-cursor'),
level: document.getElementById('viz-level'),
dedt_cursor: document.getElementById('viz-dedt-cursor'),
wave: document.getElementById('viz-wave'),
line_e: document.getElementById('line-e'),
line_dedt: document.getElementById('line-dedt'),
centroid: document.getElementById('centroid-group'),
defuzz_area: document.getElementById('defuzz-area'),
txt_out: document.getElementById('txt-out-val'),
valve: document.getElementById('valve-core'),
flow: document.getElementById('flow-effect'),
sl_e: document.getElementById('slider-e'),
sl_dedt: document.getElementById('slider-dedt'),
v_e: document.getElementById('val-e'),
v_dedt: document.getElementById('val-dedt'),
mode_text: document.getElementById('mode-text'),
mode_dot: document.getElementById('mode-dot')
};
// SVG 内部绘图范围
const PX_WIDTH = 520;
const PX_H = 100;
const MAP_E = (val) => ((val + 100) / 200) * PX_WIDTH;
const MAP_D = (val) => ((val + 100) / 200) * PX_WIDTH;
let autoMode = true;
let manualTimeout = null;
let time = 0;
function updateVisualization(e_val, dedt_val) {
// 1. 更新传感器视觉
let e_px = MAP_E(e_val);
let d_px = MAP_D(dedt_val);
// 液位高度 (-100到100 映射到 height 200到0)
let lh = 100 - (e_val/100)*100;
els.level.setAttribute('y', lh);
els.level.setAttribute('height', 200 - lh);
els.e_cursor.setAttribute('transform', `translate(0, ${lh - 100})`);
// 变化率指示
els.dedt_cursor.setAttribute('transform', `translate(0, ${-dedt_val/100 * 60})`);
els.wave.setAttribute('d', `M 0 60 Q 20 ${60 - dedt_val/100*40}, 40 60 T 80 60`);
// 2. 模糊化计算与高亮
let mu_e = getMemberships(e_val);
let mu_d = getMemberships(dedt_val);
els.line_e.setAttribute('x1', e_px); els.line_e.setAttribute('x2', e_px);
els.line_dedt.setAttribute('x1', d_px); els.line_dedt.setAttribute('x2', d_px);
for(let i=0; i<7; i++) {
document.getElementById(`ae-${i}`).setAttribute('opacity', mu_e[i] * 0.8);
document.getElementById(`ad-${i}`).setAttribute('opacity', mu_d[i] * 0.8);
}
// 3. 规则推导 (Mamdani 最小-最大法)
let out_mems = [0,0,0,0,0,0,0];
for(let i=0; i<7; i++) {
for(let j=0; j<7; j++) {
let fireStrength = Math.min(mu_e[i], mu_d[j]);
if (fireStrength > 0) {
let outIndex = ruleMatrix[i][j];
out_mems[outIndex] = Math.max(out_mems[outIndex], fireStrength);
}
}
}
// 4. 去模糊化可视化 (重心法近似)
let numerator = 0;
let denominator = 0;
// 生成聚合后的 SVG Path (粗略近似,用于视觉表达)
let pathD = "M0,150 ";
const steps = 60;
for(let k=0; k<=steps; k++) {
let x_px = (k/steps) * PX_WIDTH;
let x_val = -100 + (k/steps)*200;
let local_mu = getMemberships(x_val);
let max_val = 0;
for(let i=0; i<7; i++) {
let active_h = Math.min(local_mu[i], out_mems[i]);
max_val = Math.max(max_val, active_h);
}
let y_px = 150 - (max_val * 120); // 120是高度基数
pathD += `L${x_px},${y_px} `;
// 重心积分
numerator += x_val * max_val;
denominator += max_val;
}
pathD += `L${PX_WIDTH},150 Z`;
els.defuzz_area.setAttribute('d', pathD);
// 计算最终输出
let final_u = 0;
if (denominator > 0.01) {
final_u = numerator / denominator;
}
// 5. 更新输出视觉
let out_px = MAP_E(final_u);
els.centroid.setAttribute('transform', `translate(${out_px}, 0)`);
els.txt_out.textContent = final_u.toFixed(1) + '%';
// 阀门旋转映射: -100 到 100 映射到 -90度 到 90度
let rot = (final_u / 100) * 90;
els.valve.setAttribute('transform', `rotate(${rot}, 60, 90)`);
// 流量特效强度
let flowOp = Math.abs(final_u) / 100;
els.flow.setAttribute('opacity', flowOp);
if(final_u < 0) els.flow.setAttribute('stroke', '#ff0055');
else els.flow.setAttribute('stroke', '#00ffaa');
}
// 自动仿真循环
function simulate() {
if (autoMode) {
time += 0.02;
// 构造复杂的复合波形来模拟系统扰动
// 刻意制造 e在0附近,但dedt很大的"突变前夕"场景,体现模糊控制优势
let e_sim = Math.sin(time) * 40 + Math.sin(time * 2.5) * 20;
// 变化率是导数,通过差分或解析导数计算
let dedt_sim = Math.cos(time) * 40 + Math.cos(time * 2.5) * 50;
// 限制在 -100 到 100
e_sim = Math.max(-100, Math.min(100, e_sim));
dedt_sim = Math.max(-100, Math.min(100, dedt_sim));
// 更新滑块UI
els.sl_e.value = e_sim;
els.sl_dedt.value = dedt_sim;
els.v_e.textContent = e_sim.toFixed(1);
els.v_dedt.textContent = dedt_sim.toFixed(1);
updateVisualization(e_sim, dedt_sim);
}
requestAnimationFrame(simulate);
}
// 交互接管处理
function handleInteraction() {
autoMode = false;
els.mode_text.textContent = "手动干预模式 (5秒后恢复自动)";
els.mode_text.style.color = "#00f0ff";
els.mode_dot.style.background = "#00f0ff";
els.mode_dot.style.boxShadow = "0 0 8px #00f0ff";
let e_val = parseFloat(els.sl_e.value);
let dedt_val = parseFloat(els.sl_dedt.value);
els.v_e.textContent = e_val.toFixed(1);
els.v_dedt.textContent = dedt_val.toFixed(1);
updateVisualization(e_val, dedt_val);
clearTimeout(manualTimeout);
manualTimeout = setTimeout(() => {
autoMode = true;
els.mode_text.textContent = "仿真自动运行中 (触控滑块接管)";
els.mode_text.style.color = "var(--accent-amber)";
els.mode_dot.style.background = "var(--accent-amber)";
els.mode_dot.style.boxShadow = "0 0 8px var(--accent-amber)";
}, 5000);
}
els.sl_e.addEventListener('input', handleInteraction);
els.sl_dedt.addEventListener('input', handleInteraction);
// 初始化启动 (确保DOMContentLoaded后无条件自动播放)
document.addEventListener('DOMContentLoaded', () => {
simulate();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PLC模糊逻辑自适应控制系统原理图</title>
<style>
:root {
--bg-color: #0b0f19;
--panel-bg: rgba(16, 24, 43, 0.8);
--border-color: #2a3b5c;
--text-main: #e2e8f0;
--text-muted: #64748b;
--color-e: #00f0ff; /* 偏差 Cyan */
--color-dedt: #ff007a; /* 变化率 Magenta */
--color-out: #00ff88; /* 输出 Green */
--color-water: #3b82f6;
--font-mono: 'Courier New', Courier, monospace;
}
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
background-color: var(--bg-color);
color: var(--text-main);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 100vh;
overflow: hidden;
background-image:
radial-gradient(circle at 50% 0%, rgba(0, 240, 255, 0.05) 0%, transparent 50%),
radial-gradient(circle at 50% 100%, rgba(0, 255, 136, 0.05) 0%, transparent 50%);
}
.dashboard {
width: 100%;
max-width: 1200px;
background: var(--panel-bg);
border: 1px solid var(--border-color);
border-radius: 12px;
box-shadow: 0 0 40px rgba(0,0,0,0.5), inset 0 0 20px rgba(0, 240, 255, 0.05);
backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
position: relative;
}
.header {
padding: 20px 30px;
border-bottom: 1px solid var(--border-color);
display: flex;
justify-content: space-between;
align-items: center;
}
.title-group h1 {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: 1px;
margin-bottom: 4px;
background: linear-gradient(90deg, #fff, #8cb4ff);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.title-group p {
color: var(--text-muted);
font-size: 0.85rem;
}
.controls {
display: flex;
gap: 15px;
align-items: center;
}
.btn {
background: rgba(42, 59, 92, 0.5);
border: 1px solid var(--border-color);
color: var(--text-main);
padding: 8px 16px;
border-radius: 6px;
cursor: pointer;
font-size: 0.85rem;
transition: all 0.3s ease;
}
.btn:hover { background: rgba(42, 59, 92, 0.9); }
.btn.active { border-color: var(--color-e); box-shadow: 0 0 10px rgba(0, 240, 255, 0.3); }
.animation-container {
position: relative;
width: 100%;
aspect-ratio: 16 / 9;
padding: 20px;
}
svg {
width: 100%;
height: 100%;
display: block;
}
/* SVG Styling */
.flow-line {
fill: none;
stroke: var(--border-color);
stroke-width: 2;
}
.flow-anim {
fill: none;
stroke-width: 2;
stroke-dasharray: 6 6;
animation: flow 1s linear infinite;
}
.flow-e { stroke: var(--color-e); }
.flow-dedt { stroke: var(--color-dedt); }
.flow-out { stroke: var(--color-out); }
@keyframes flow { to { stroke-dashoffset: -12; } }
.fuzzy-chart-bg { fill: rgba(255,255,255,0.02); stroke: rgba(255,255,255,0.1); stroke-width: 1; }
.fuzzy-axis { stroke: rgba(255,255,255,0.2); stroke-width: 1; }
.fuzzy-text { fill: var(--text-muted); font-size: 10px; font-family: var(--font-mono); text-anchor: middle; }
/* Fuzzy Sets */
.mf-path { fill: none; stroke-width: 1.5; opacity: 0.4; }
.mf-path.active { opacity: 1; stroke-width: 2; filter: drop-shadow(0 0 4px currentColor); }
.mf-e { stroke: var(--color-e); }
.mf-dedt { stroke: var(--color-dedt); }
.mf-out { stroke: var(--color-out); }
.indicator-line { stroke-width: 2; filter: drop-shadow(0 0 5px currentColor); transition: all 0.1s; }
.val-text { font-family: var(--font-mono); font-weight: bold; font-size: 14px; fill: #fff; }
.label-text { font-size: 12px; fill: var(--text-muted); font-weight: bold; }
.glow-box {
fill: rgba(16, 24, 43, 0.6);
stroke: var(--border-color);
stroke-width: 1.5;
rx: 8;
}
.triz-highlight {
animation: trizPulse 2s infinite alternate;
}
@keyframes trizPulse {
0% { filter: drop-shadow(0 0 2px var(--color-out)); }
100% { filter: drop-shadow(0 0 15px var(--color-out)); }
}
/* Sliders overlay */
.slider-panel {
position: absolute;
bottom: 30px;
left: 30px;
background: rgba(10, 15, 25, 0.8);
border: 1px solid var(--border-color);
padding: 15px;
border-radius: 8px;
backdrop-filter: blur(5px);
display: none; /* Hidden in auto mode */
flex-direction: column;
gap: 15px;
}
.slider-panel.visible { display: flex; }
.slider-row { display: flex; align-items: center; gap: 10px; }
.slider-row label { width: 80px; font-size: 12px; color: var(--text-muted); }
input[type=range] {
-webkit-appearance: none;
width: 150px;
background: transparent;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
height: 14px; width: 14px;
border-radius: 50%;
background: #fff;
cursor: pointer;
margin-top: -6px;
}
input[type=range]::-webkit-slider-runnable-track {
width: 100%; height: 3px;
background: var(--border-color);
border-radius: 2px;
}
#slider-disturbance::-webkit-slider-thumb { box-shadow: 0 0 8px var(--color-water); }
#slider-target::-webkit-slider-thumb { box-shadow: 0 0 8px var(--text-main); }
</style>
</head>
<body>
<div class="dashboard">
<div class="header">
<div class="title-group">
<h1>PLC 模糊逻辑自适应控制引擎</h1>
<p>TRIZ 最终理想解: 利用现有偏差与变化率资源,实现无需阈值开关的平滑连续调节,提前消除剧烈波动。</p>
</div>
<div class="controls">
<button class="btn active" id="btn-auto">自动扰动演示</button>
<button class="btn" id="btn-manual">手动交互</button>
</div>
</div>
<div class="animation-container">
<svg viewBox="0 0 1200 675" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="waterGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="rgba(59, 130, 246, 0.8)" />
<stop offset="100%" stop-color="rgba(29, 78, 216, 0.9)" />
</linearGradient>
<linearGradient id="tankGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="rgba(255,255,255,0.05)" />
<stop offset="50%" stop-color="rgba(255,255,255,0.01)" />
<stop offset="100%" stop-color="rgba(255,255,255,0.05)" />
</linearGradient>
<radialGradient id="glowE" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="var(--color-e)" stop-opacity="0.3"/>
<stop offset="100%" stop-color="var(--color-e)" stop-opacity="0"/>
</radialGradient>
<radialGradient id="glowOut" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="var(--color-out)" stop-opacity="0.3"/>
<stop offset="100%" stop-color="var(--color-out)" stop-opacity="0"/>
</radialGradient>
<!-- Fuzzy Set Definitions (Triangles) -->
<g id="fuzzy-sets-template">
<path d="M 0 50 L 16.6 0 L 33.3 50" class="mf-path mf-tpl" data-rule="NM" />
<path d="M 16.6 50 L 33.3 0 L 50 50" class="mf-path mf-tpl" data-rule="NS" />
<path d="M 33.3 50 L 50 0 L 66.6 50" class="mf-path mf-tpl" data-rule="Z" />
<path d="M 50 50 L 66.6 0 L 83.3 50" class="mf-path mf-tpl" data-rule="PS" />
<path d="M 66.6 50 L 83.3 0 L 100 50" class="mf-path mf-tpl" data-rule="PM" />
<!-- Edges (Trapezoids/Triangles) -->
<path d="M 0 0 L 16.6 0 L 33.3 50 L 0 50 Z" class="mf-path mf-tpl" data-rule="NB" />
<path d="M 66.6 50 L 83.3 0 L 100 0 L 100 50 Z" class="mf-path mf-tpl" data-rule="PB" />
</g>
</defs>
<!-- Background Elements -->
<rect x="0" y="0" width="1200" height="675" fill="transparent" />
<!-- ==================== STAGE 1: PHYSICAL SYSTEM (Left) ==================== -->
<g transform="translate(50, 150)">
<text x="75" y="-20" class="label-text" text-anchor="middle">受控对象 (液位)</text>
<!-- Tank -->
<rect x="0" y="0" width="150" height="300" fill="url(#tankGrad)" stroke="var(--border-color)" stroke-width="2" rx="10" />
<!-- Target Line -->
<line x1="-10" y1="150" x2="160" y2="150" stroke="var(--text-muted)" stroke-width="1.5" stroke-dasharray="4 4" />
<text x="165" y="154" fill="var(--text-muted)" font-size="12">SV (目标)</text>
<!-- Water (Animated) -->
<rect id="water-level" x="2" y="150" width="146" height="148" fill="url(#waterGrad)" rx="8" />
<!-- Sensor -->
<circle cx="150" cy="150" r="4" fill="var(--color-e)" id="sensor-point" />
<path d="M 150 150 L 190 150 L 190 -50 L 250 -50" class="flow-line" />
<path d="M 150 150 L 190 150 L 190 -50 L 250 -50" class="flow-anim flow-e" />
<text x="75" y="325" fill="var(--text-muted)" font-size="12" text-anchor="middle">扰动波模拟</text>
</g>
<!-- ==================== STAGE 2: PLC FUZZY CONTROLLER (Center) ==================== -->
<g transform="translate(320, 50)">
<!-- PLC Chassis -->
<rect x="0" y="0" width="560" height="520" class="glow-box" />
<text x="280" y="30" class="label-text" fill="#fff" font-size="16" text-anchor="middle">PLC 内部模糊控制构型 (IEC 61131-3)</text>
<!-- Data Interface / Processing -->
<g transform="translate(20, 60)">
<rect x="0" y="0" width="120" height="50" fill="rgba(0, 240, 255, 0.05)" stroke="var(--color-e)" stroke-width="1" rx="4" />
<text x="60" y="20" class="label-text" text-anchor="middle" fill="var(--color-e)">液位偏差 (e)</text>
<text x="60" y="40" class="val-text" id="val-e" fill="var(--color-e)" text-anchor="middle">0.0 %</text>
<rect x="0" y="70" width="120" height="50" fill="rgba(255, 0, 122, 0.05)" stroke="var(--color-dedt)" stroke-width="1" rx="4" />
<text x="60" y="90" class="label-text" text-anchor="middle" fill="var(--color-dedt)">变化率 (de/dt)</text>
<text x="60" y="110" class="val-text" id="val-dedt" fill="var(--color-dedt)" text-anchor="middle">0.0 %/s</text>
<!-- Flow lines from inputs to fuzzification -->
<path d="M 120 25 L 160 25" class="flow-line" /><path d="M 120 25 L 160 25" class="flow-anim flow-e" />
<path d="M 120 95 L 160 95" class="flow-line" /><path d="M 120 95 L 160 95" class="flow-anim flow-dedt" />
</g>
<!-- FUZZIFICATION CHARTS (e and de/dt) -->
<g transform="translate(200, 50)">
<text x="0" y="-10" class="label-text">1. 模糊化 (隶属度函数)</text>
<!-- Chart E -->
<g transform="translate(0, 0)">
<rect x="0" y="0" width="160" height="60" class="fuzzy-chart-bg" />
<line x1="0" y1="60" x2="160" y2="60" class="fuzzy-axis" />
<!-- Labels -->
<g transform="translate(0, 75)">
<text x="0" y="0" class="fuzzy-text">NB</text><text x="26.6" y="0" class="fuzzy-text">NM</text>
<text x="53.3" y="0" class="fuzzy-text">NS</text><text x="80" y="0" class="fuzzy-text">Z</text>
<text x="106.6" y="0" class="fuzzy-text">PS</text><text x="133.3" y="0" class="fuzzy-text">PM</text>
<text x="160" y="0" class="fuzzy-text">PB</text>
</g>
<!-- Scale template to fit 160x60 (1.6x X, 1.2x Y) -->
<g transform="scale(1.6, 1.2)" class="mf-e">
<use href="#fuzzy-sets-template" />
</g>
<!-- Indicator -->
<line x1="80" y1="0" x2="80" y2="60" id="ind-e" class="indicator-line" stroke="var(--color-e)" />
<circle cx="80" cy="60" r="3" fill="var(--color-e)" id="dot-e" />
</g>
<!-- Chart DE/DT -->
<g transform="translate(0, 110)">
<rect x="0" y="0" width="160" height="60" class="fuzzy-chart-bg" />
<line x1="0" y1="60" x2="160" y2="60" class="fuzzy-axis" />
<g transform="scale(1.6, 1.2)" class="mf-dedt">
<use href="#fuzzy-sets-template" />
</g>
<line x1="80" y1="0" x2="80" y2="60" id="ind-dedt" class="indicator-line" stroke="var(--color-dedt)" />
<circle cx="80" cy="60" r="3" fill="var(--color-dedt)" id="dot-dedt" />
</g>
<!-- Flow to Matrix -->
<path d="M 160 30 L 190 30 L 190 90 L 220 90" class="flow-line" /><path d="M 160 30 L 190 30 L 190 90 L 220 90" class="flow-anim flow-e" />
<path d="M 160 140 L 190 140 L 190 100 L 220 100" class="flow-line" /><path d="M 160 140 L 190 140 L 190 100 L 220 100" class="flow-anim flow-dedt" />
</g>
<!-- FUZZY RULES MATRIX (Inference) -->
<g transform="translate(430, 115)">
<text x="45" y="-30" class="label-text" text-anchor="middle">2. 模糊推理引擎</text>
<rect x="0" y="-15" width="90" height="80" fill="rgba(255,255,255,0.03)" stroke="var(--border-color)" rx="5" />
<!-- Decorative Matrix nodes -->
<circle cx="20" cy="5" r="2" fill="var(--text-muted)"/><circle cx="45" cy="5" r="2" fill="var(--text-muted)"/><circle cx="70" cy="5" r="2" fill="var(--text-muted)"/>
<circle cx="20" cy="25" r="2" fill="var(--text-muted)"/><circle cx="45" cy="25" r="4" fill="#fff" filter="drop-shadow(0 0 5px #fff)" id="active-rule-node" /><circle cx="70" cy="25" r="2" fill="var(--text-muted)"/>
<circle cx="20" cy="45" r="2" fill="var(--text-muted)"/><circle cx="45" cy="45" r="2" fill="var(--text-muted)"/><circle cx="70" cy="45" r="2" fill="var(--text-muted)"/>
<text x="45" y="75" font-size="10" fill="var(--text-muted)" text-anchor="middle">7x7 规则矩阵</text>
<!-- Dynamic text indicating the key mechanism -->
<rect x="-40" y="95" width="170" height="40" fill="rgba(0,0,0,0.5)" rx="4" stroke="rgba(255,255,255,0.1)"/>
<text x="45" y="110" font-size="11" fill="var(--color-out)" text-anchor="middle" id="rule-desc1">常规调节中...</text>
<text x="45" y="125" font-size="11" fill="var(--text-main)" text-anchor="middle" id="rule-desc2">防止超调</text>
<!-- Flow to Defuzz -->
<path d="M 45 135 L 45 180 L -150 180 L -150 200" class="flow-line" />
<path d="M 45 135 L 45 180 L -150 180 L -150 200" class="flow-anim flow-out" />
</g>
<!-- DEFUZZIFICATION (Centroid) -->
<g transform="translate(180, 310)">
<text x="100" y="-10" class="label-text" text-anchor="middle">3. 重心法解模糊 (去模糊化)</text>
<rect x="0" y="0" width="200" height="100" class="fuzzy-chart-bg" />
<line x1="0" y1="100" x2="200" y2="100" class="fuzzy-axis" />
<g transform="translate(0, 115)">
<text x="0" y="0" class="fuzzy-text">全闭</text>
<text x="100" y="0" class="fuzzy-text">保持 (50%)</text>
<text x="200" y="0" class="fuzzy-text">全开</text>
</g>
<!-- Output Fuzzy Sets Background -->
<g transform="scale(2.0, 2.0)" class="mf-out">
<use href="#fuzzy-sets-template" />
</g>
<!-- The dynamic Defuzzification Polygon (Simulated for visual effect) -->
<polygon id="defuzz-poly" points="0,100 200,100" fill="rgba(0, 255, 136, 0.3)" stroke="none" />
<!-- Center of Gravity (COG) Line - The key TRIZ continuous output -->
<g id="cog-group" transform="translate(100, 0)">
<line x1="0" y1="0" x2="0" y2="100" stroke="var(--color-out)" stroke-width="3" class="triz-highlight" />
<polygon points="-5,0 5,0 0,8" fill="var(--color-out)" />
<text x="0" y="-10" fill="var(--color-out)" font-size="12" font-weight="bold" text-anchor="middle" id="val-cog">50.0%</text>
<text x="0" y="-25" fill="var(--color-out)" font-size="10" text-anchor="middle">连续调节量</text>
</g>
<!-- Output Flow out of PLC -->
<path d="M 200 50 L 380 50 L 380 -100 L 480 -100" class="flow-line" />
<path d="M 200 50 L 380 50 L 380 -100 L 480 -100" class="flow-anim flow-out" />
</g>
</g>
<!-- ==================== STAGE 3: ACTUATOR (Right) ==================== -->
<g transform="translate(900, 200)">
<text x="50" y="-40" class="label-text" text-anchor="middle">执行机构 (连续调节阀)</text>
<!-- Pipe -->
<rect x="-50" y="40" width="200" height="20" fill="rgba(255,255,255,0.1)" stroke="var(--border-color)" />
<rect id="pipe-flow" x="-50" y="42" width="200" height="16" fill="var(--color-water)" opacity="0.5" />
<!-- Valve Body -->
<path d="M 20 10 L 80 10 L 50 40 Z" fill="var(--border-color)" />
<path d="M 20 90 L 80 90 L 50 60 Z" fill="var(--border-color)" />
<rect x="40" y="40" width="20" height="20" fill="var(--panel-bg)" stroke="var(--border-color)" />
<!-- Valve Actuator (Rotates) -->
<circle cx="50" cy="50" r="15" fill="var(--bg-color)" stroke="var(--color-out)" stroke-width="2" class="triz-highlight" />
<g id="valve-indicator" transform="rotate(0, 50, 50)">
<line x1="50" y1="35" x2="50" y2="65" stroke="var(--color-out)" stroke-width="4" />
</g>
<!-- Value Display -->
<rect x="10" y="110" width="80" height="30" fill="rgba(0, 255, 136, 0.1)" stroke="var(--color-out)" rx="4" />
<text x="50" y="130" class="val-text" fill="var(--color-out)" text-anchor="middle" id="val-valve">50.0%</text>
<text x="50" y="155" font-size="11" fill="var(--text-muted)" text-anchor="middle">开度</text>
<!-- Water drops falling (visual effect of flow) -->
<g id="water-drops" transform="translate(160, 40)">
<!-- Animated via JS -->
</g>
<path d="M 150 50 L 220 50 L 220 220 L -800 220 L -800 130" class="flow-line" />
<path d="M 150 50 L 220 50 L 220 220 L -800 220 L -800 130" class="flow-anim flow-out" style="stroke: rgba(59, 130, 246, 0.5);" />
</g>
</svg>
</div>
<div class="slider-panel" id="manual-controls">
<div class="slider-row">
<label>注入浊波扰动</label>
<input type="range" id="slider-disturbance" min="-40" max="40" value="0">
</div>
<div class="slider-row">
<label>目标液位 (SV)</label>
<input type="range" id="slider-target" min="20" max="80" value="50">
</div>
<p style="font-size: 11px; color: var(--text-muted); margin-top: 5px;">* 手动控制扰动,观察模糊逻辑如何处理“小偏差、大变化率”的情况。</p>
</div>
</div>
<script>
document.addEventListener("DOMContentLoaded", () => {
// DOM Elements
const elWaterLevel = document.getElementById('water-level');
const elValE = document.getElementById('val-e');
const elValDedt = document.getElementById('val-dedt');
const elIndE = document.getElementById('ind-e');
const elDotE = document.getElementById('dot-e');
const elIndDedt = document.getElementById('ind-dedt');
const elDotDedt = document.getElementById('dot-dedt');
const elCogGroup = document.getElementById('cog-group');
const elValCog = document.getElementById('val-cog');
const elDefuzzPoly = document.getElementById('defuzz-poly');
const elValveInd = document.getElementById('valve-indicator');
const elValValve = document.getElementById('val-valve');
const elPipeFlow = document.getElementById('pipe-flow');
const elRuleDesc1 = document.getElementById('rule-desc1');
const elRuleDesc2 = document.getElementById('rule-desc2');
const elRuleNode = document.getElementById('active-rule-node');
const btnAuto = document.getElementById('btn-auto');
const btnManual = document.getElementById('btn-manual');
const manualControls = document.getElementById('manual-controls');
const sliderDisturbance = document.getElementById('slider-disturbance');
const sliderTarget = document.getElementById('slider-target');
// System State
let isAutoMode = true;
let time = 0;
let targetLevel = 50; // 0 to 100
let currentLevel = 50;
let lastLevel = 50;
let manualDisturbance = 0;
// Fuzzy Logic Simulator Configuration
// Domain: -100 to 100 mapping to 0 to 160 (SVG width) for visualization
const domainWidth = 160;
// Membership Functions Centers (-100 to 100 scale logically)
const mfs = [
{ name: 'NB', center: -100 },
{ name: 'NM', center: -66.6 },
{ name: 'NS', center: -33.3 },
{ name: 'Z', center: 0 },
{ name: 'PS', center: 33.3 },
{ name: 'PM', center: 66.6 },
{ name: 'PB', center: 100 }
];
// Simplistic Rule Base Mapping (e, dedt -> output)
// To demonstrate the specific TRIZ capability:
// If e is Z (small) but dedt is PB (fast rise), output should be NB (close valve early)
function mapValueToSVG(val, rangeMin, rangeMax, svgWidth) {
// Clamp and map
let normalized = (val - rangeMin) / (rangeMax - rangeMin);
normalized = Math.max(0, Math.min(1, normalized));
return normalized * svgWidth;
}
// Animation Loop
function animate() {
// 1. Process Inputs based on mode
if (isAutoMode) {
// Auto Mode: Simulate a sine wave disturbance + sudden spikes
let sineWave = Math.sin(time * 0.03) * 15;
let spike = 0;
// Generate a sudden "turbid wave" every ~300 frames
if (time % 400 > 350) {
spike = Math.sin((time % 400 - 350) * 0.1) * 30;
}
// System inertia/physics simulation
let naturalLevel = targetLevel + sineWave + spike;
// Valve effect (valve > 50 increases level, < 50 decreases)
let currentValve = parseFloat(elValCog.textContent) || 50;
let valveEffect = (currentValve - 50) * 0.1;
// Basic low-pass filter to simulate water mass
currentLevel = currentLevel * 0.95 + (naturalLevel + valveEffect*10) * 0.05;
} else {
// Manual Mode
targetLevel = parseFloat(sliderTarget.value);
let dist = parseFloat(sliderDisturbance.value);
// Decay manual disturbance back to 0 slowly if user lets go (handled by slider input intuitively)
currentLevel = currentLevel * 0.9 + (targetLevel + dist) * 0.1;
}
// 2. Calculate e and de/dt (Logical bounds: e [-50, 50], dedt [-10, 10])
let e = targetLevel - currentLevel; // Error
let dedt = currentLevel - lastLevel; // Rate of change
lastLevel = currentLevel;
// Scale up logically for the -100 to 100 fuzzy domain
let logical_e = Math.max(-100, Math.min(100, e * 4));
let logical_dedt = Math.max(-100, Math.min(100, dedt * 30));
// 3. Update Input Visuals
let svg_e_x = mapValueToSVG(logical_e, -100, 100, domainWidth);
elIndE.setAttribute('x1', svg_e_x);
elIndE.setAttribute('x2', svg_e_x);
elDotE.setAttribute('cx', svg_e_x);
elValE.textContent = e.toFixed(1) + '%';
let svg_dedt_x = mapValueToSVG(logical_dedt, -100, 100, domainWidth);
elIndDedt.setAttribute('x1', svg_dedt_x);
elIndDedt.setAttribute('x2', svg_dedt_x);
elDotDedt.setAttribute('cx', svg_dedt_x);
elValDedt.textContent = dedt.toFixed(2) + '%/s';
// 4. Fuzzy Inference Simulation (Simplified for visual narrative)
// Standard PD-like fuzzy control: Output = KP*e + KD*dedt
// We want to highlight the TRIZ feature: early action on high dedt even when e is small.
let rawOutput = (logical_e * 0.4) - (logical_dedt * 0.8);
// Clamp output to -100 to 100
let logical_out = Math.max(-100, Math.min(100, rawOutput));
// Convert to 0-100% valve opening
let valveOpening = (logical_out + 100) / 2;
// Visualizing the "Rule Firing" narrative
if (Math.abs(logical_e) < 20 && Math.abs(logical_dedt) > 40) {
elRuleDesc1.textContent = "偏差小,但变化率剧烈!";
elRuleDesc1.style.fill = "var(--color-dedt)";
elRuleDesc2.textContent = "触发模糊前馈,提前动作";
elRuleNode.setAttribute('fill', 'var(--color-dedt)');
elRuleNode.setAttribute('filter', 'drop-shadow(0 0 8px var(--color-dedt))');
} else if (Math.abs(logical_e) > 50) {
elRuleDesc1.textContent = "偏差大,主导调节";
elRuleDesc1.style.fill = "var(--color-e)";
elRuleDesc2.textContent = "快速逼近目标值";
elRuleNode.setAttribute('fill', 'var(--color-e)');
elRuleNode.setAttribute('filter', 'drop-shadow(0 0 8px var(--color-e))');
} else {
elRuleDesc1.textContent = "稳态微调中...";
elRuleDesc1.style.fill = "var(--color-out)";
elRuleDesc2.textContent = "连续平滑输出,无震荡";
elRuleNode.setAttribute('fill', '#fff');
elRuleNode.setAttribute('filter', 'drop-shadow(0 0 5px #fff)');
}
// 5. Update Output / Defuzzification Visuals
// Map logical_out (-100 to 100) to SVG width (200px)
let out_x = mapValueToSVG(logical_out, -100, 100, 200);
// Animate COG group
elCogGroup.setAttribute('transform', `translate(${out_x}, 0)`);
elValCog.textContent = valveOpening.toFixed(1) + '%';
elValValve.textContent = valveOpening.toFixed(1) + '%';
// Animate the dynamic polygon to simulate the defuzzification area
// Shape narrows as it approaches extremes, wider in middle
let spread = 40 + (100 - Math.abs(logical_out)) * 0.4;
let p1x = Math.max(0, out_x - spread);
let p2x = Math.min(200, out_x + spread);
elDefuzzPoly.setAttribute('points', `0,100 ${p1x},100 ${out_x},${100 - spread*0.5} ${p2x},100 200,100`);
// 6. Update Actuator Visuals
// Rotate valve indicator (0% = -90deg, 100% = 0deg)
let rotation = (valveOpening / 100) * 90 - 90;
elValveInd.setAttribute('transform', `rotate(${rotation}, 50, 50)`);
// Pipe flow opacity based on valve
elPipeFlow.style.opacity = 0.1 + (valveOpening / 100) * 0.8;
// 7. Update Tank Visuals
// Tank total height is 148. Water goes from y=150 (empty) to y=2 (full)
// 0% level = y:150, height:0. 100% level = y:2, height:148
let h = (currentLevel / 100) * 148;
let y = 150 - h;
// SVG starts drawing rect from top-left, so we change Y and Height
elWaterLevel.setAttribute('y', y);
elWaterLevel.setAttribute('height', Math.max(0, h));
time++;
requestAnimationFrame(animate);
}
// Event Listeners
btnAuto.addEventListener('click', () => {
isAutoMode = true;
btnAuto.classList.add('active');
btnManual.classList.remove('active');
manualControls.classList.remove('visible');
sliderDisturbance.value = 0; // reset
});
btnManual.addEventListener('click', () => {
isAutoMode = false;
btnManual.classList.add('active');
btnAuto.classList.remove('active');
manualControls.classList.add('visible');
// Sync sliders with current auto state to avoid jumps
sliderTarget.value = targetLevel;
currentLevel = 50; // reset to middle for manual play
targetLevel = 50;
});
// Start Animation immediately (Requirements 6 & 7)
animate();
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>基于 PLC 内嵌模糊逻辑的自适应液位控制系统 (IFR)</title>
<style>
:root {
--bg: #05050a;
--panel: rgba(15, 23, 42, 0.7);
--cyan: #00f3ff;
--magenta: #ff00ea;
--green: #00ff66;
--text-main: #f8fafc;
--text-sub: #94a3b8;
--border: #334155;
}
body {
background-color: var(--bg);
margin: 0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
font-family: system-ui, -apple-system, sans-serif;
color: var(--text-main);
overflow: hidden;
}
.wrapper {
position: relative;
width: 100%;
max-width: 1200px;
aspect-ratio: 1200 / 650;
box-shadow: 0 0 50px rgba(0, 243, 255, 0.05);
border: 1px solid rgba(255, 255, 255, 0.05);
border-radius: 16px;
background: #0a0b10;
}
svg {
width: 100%;
height: 100%;
display: block;
}
/* SVG Effects */
.glow-cyan { filter: drop-shadow(0 0 8px var(--cyan)); }
.glow-magenta { filter: drop-shadow(0 0 8px var(--magenta)); }
.glow-green { filter: drop-shadow(0 0 8px var(--green)); }
.data-bus {
fill: none;
stroke: var(--cyan);
stroke-width: 2.5;
stroke-dasharray: 8 6;
animation: dashFlow 0.8s linear infinite;
filter: drop-shadow(0 0 5px var(--cyan));
}
@keyframes dashFlow {
from { stroke-dashoffset: 14; }
to { stroke-dashoffset: 0; }
}
/* UI Controls Overlay */
.controls-overlay {
position: absolute;
bottom: 30px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 40px;
align-items: center;
background: rgba(15, 23, 42, 0.85);
padding: 15px 40px;
border-radius: 100px;
border: 1px solid var(--border);
backdrop-filter: blur(12px);
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.6);
z-index: 10;
}
.action-btn {
appearance: none;
border: 1px solid #ef4444;
background: rgba(239, 68, 68, 0.1);
color: #ef4444;
font-size: 15px;
font-weight: bold;
padding: 10px 24px;
border-radius: 50px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
}
.action-btn:hover {
background: #ef4444;
color: #fff;
box-shadow: 0 0 20px rgba(239, 68, 68, 0.5);
transform: translateY(-2px);
}
.action-btn:active {
transform: translateY(1px);
}
.slider-box {
display: flex;
flex-direction: column;
gap: 6px;
color: var(--text-sub);
font-size: 14px;
}
input[type=range] {
width: 180px;
accent-color: var(--green);
cursor: pointer;
}
/* HTML embedded via foreignObject */
.ifr-box {
font-family: system-ui, -apple-system, sans-serif;
color: var(--text-sub);
font-size: 13px;
line-height: 1.6;
background: rgba(15, 23, 42, 0.85);
padding: 16px;
border-radius: 12px;
border: 1px solid rgba(0, 243, 255, 0.3);
box-shadow: 0 10px 30px rgba(0, 243, 255, 0.1);
backdrop-filter: blur(10px);
}
.ifr-title {
margin: 0 0 10px 0;
color: var(--cyan);
font-size: 15px;
display: flex;
align-items: center;
gap: 6px;
border-bottom: 1px solid rgba(0, 243, 255, 0.2);
padding-bottom: 8px;
}
</style>
</head>
<body>
<div class="wrapper">
<svg viewBox="0 0 1200 650" id="main-svg">
<defs>
<!-- Background Grid Pattern -->
<pattern id="grid" width="40" height="40" patternUnits="userSpaceOnUse">
<path d="M 40 0 L 0 0 0 40" fill="none" stroke="#1e293b" stroke-width="0.5" opacity="0.5"/>
</pattern>
<!-- Water Gradient -->
<linearGradient id="water-grad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#00f3ff" stop-opacity="0.8"/>
<stop offset="100%" stop-color="#0284c7" stop-opacity="0.95"/>
</linearGradient>
<!-- Fuzzy Sets Background Template (7 Segments) -->
<g id="fuzzy-sets-template">
<line x1="0" y1="40" x2="300" y2="40" stroke="#334155" stroke-width="1.5"/>
<!-- NB --> <polyline points="0,0 25,0 50,40" fill="none" stroke="#ef4444" stroke-width="1.5" opacity="0.6"/>
<!-- NM --> <polyline points="0,40 50,0 100,40" fill="none" stroke="#f97316" stroke-width="1.5" opacity="0.6"/>
<!-- NS --> <polyline points="50,40 100,0 150,40" fill="none" stroke="#eab308" stroke-width="1.5" opacity="0.6"/>
<!-- ZO --> <polyline points="100,40 150,0 200,40" fill="none" stroke="#22c55e" stroke-width="1.5" opacity="0.6"/>
<!-- PS --> <polyline points="150,40 200,0 250,40" fill="none" stroke="#06b6d4" stroke-width="1.5" opacity="0.6"/>
<!-- PM --> <polyline points="200,40 250,0 300,40" fill="none" stroke="#3b82f6" stroke-width="1.5" opacity="0.6"/>
<!-- PB --> <polyline points="250,40 275,0 300,0" fill="none" stroke="#a855f7" stroke-width="1.5" opacity="0.6"/>
<text x="0" y="55" font-size="10" fill="#ef4444" text-anchor="middle" font-weight="bold">NB</text>
<text x="50" y="55" font-size="10" fill="#f97316" text-anchor="middle">NM</text>
<text x="100" y="55" font-size="10" fill="#eab308" text-anchor="middle">NS</text>
<text x="150" y="55" font-size="10" fill="#22c55e" text-anchor="middle" font-weight="bold">ZO</text>
<text x="200" y="55" font-size="10" fill="#06b6d4" text-anchor="middle">PS</text>
<text x="250" y="55" font-size="10" fill="#3b82f6" text-anchor="middle">PM</text>
<text x="300" y="55" font-size="10" fill="#a855f7" text-anchor="middle" font-weight="bold">PB</text>
<text x="-15" y="55" font-size="10" fill="#64748b" text-anchor="middle">-</text>
<text x="315" y="55" font-size="10" fill="#64748b" text-anchor="middle">+</text>
</g>
</defs>
<!-- Background layer -->
<rect width="100%" height="100%" fill="url(#grid)" />
<!-- IFR Info Box (Top Left) -->
<foreignObject x="30" y="25" width="340" height="220">
<div xmlns="http://www.w3.org/1999/xhtml" class="ifr-box">
<h4 class="ifr-title">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
TRIZ 最终理想解 (IFR)
</h4>
<p style="margin:0 0 8px 0;"><strong>矛盾痛点:</strong>常规阈值开关遇到剧烈浊波波动时,频繁误触发且控制剧烈震荡。</p>
<p style="margin:0 0 8px 0;"><strong>资源利用:</strong>零硬件扩容。复用现有 PLC 算力,内嵌 7段式模糊推理逻辑库。</p>
<p style="margin:0; color:var(--green);"><strong>理想运作:</strong>系统将 <em>液位偏差 (e)</em> 与 <em>偏差变化率 (de/dt)</em> 进行二维推理。在浊波突发时提前预判,输出平滑连续的执行动作,自适应滤除噪声并消除震荡。</p>
</div>
</foreignObject>
<!-- ================= SYSTEM ARCHITECTURE ================= -->
<!-- 1. Pipeline & Water Streams -->
<!-- Source pipe to valve -->
<path d="M 1150 200 L 960 200" stroke="#00f3ff" stroke-width="30" opacity="0.4"/>
<!-- Valve to Tank (Dynamic Stream) -->
<path id="in-stream" d="M 940 200 L 160 200 L 160 380" fill="none" stroke="#00f3ff" stroke-width="15" opacity="0.8" class="glow-cyan"/>
<!-- Pipe Walls -->
<path d="M 1150 180 L 140 180 L 140 370" fill="none" stroke="#334155" stroke-width="4"/>
<path d="M 1150 220 L 960 220 M 920 220 L 180 220 L 180 340" fill="none" stroke="#334155" stroke-width="4"/>
<!-- Tank Outflow -->
<path d="M 220 510 L 380 510 M 220 550 L 380 550" fill="none" stroke="#334155" stroke-width="4"/>
<path id="outflow-stream" d="M 220 530 L 380 530" fill="none" stroke="#00f3ff" stroke-width="15" stroke-linecap="round" opacity="0.7"/>
<text x="390" y="535" fill="#64748b" font-size="12">排水端</text>
<text id="disturb-warning" x="390" y="555" fill="#ef4444" font-size="14" font-weight="bold" opacity="0" style="transition: opacity 0.2s;">⚠️ 快速浊波爆发 (浪涌涌出)</text>
<!-- 2. Water Tank Component -->
<g id="tank-system">
<!-- Tank Glass -->
<rect x="100" y="350" width="120" height="200" rx="8" fill="rgba(255,255,255,0.02)" stroke="#475569" stroke-width="3"/>
<rect x="105" y="355" width="20" height="190" rx="4" fill="rgba(255,255,255,0.05)"/> <!-- Gloss -->
<!-- Dynamic Water Path -->
<path id="tank-water" d="" fill="url(#water-grad)"/>
<!-- Setpoint Indicator -->
<line id="sp-line" x1="90" y1="450" x2="230" y2="450" stroke="#00ff66" stroke-width="2" stroke-dasharray="4 4" opacity="0.8"/>
<text id="sp-text" x="235" y="454" fill="#00ff66" font-size="12" font-weight="bold">设定水位 SV</text>
<text id="pv-text" x="235" y="430" fill="#00f3ff" font-size="12">当前液位 PV</text>
<!-- Sensor -->
<g transform="translate(220, 440)">
<rect width="24" height="24" rx="4" fill="#1e293b" stroke="#00f3ff" stroke-width="1.5"/>
<circle cx="12" cy="12" r="4" fill="#00f3ff" class="glow-cyan"/>
<text x="12" y="-8" fill="#00f3ff" font-size="11" text-anchor="middle">液位变送器</text>
</g>
</g>
<!-- 3. Control Valve Component -->
<g id="valve-system" transform="translate(930, 70)">
<rect width="44" height="44" rx="6" fill="#1e293b" stroke="#ff00ea" stroke-width="2"/>
<text x="22" y="-12" fill="#ff00ea" font-size="13" text-anchor="middle" font-weight="bold">智能伺服执行器</text>
<!-- Spinning cog to represent continuous servo action -->
<circle cx="22" cy="22" r="12" fill="none" stroke="#ff00ea" stroke-dasharray="6 4">
<animateTransform attributeName="transform" type="rotate" from="0 22 22" to="360 22 22" dur="3s" repeatCount="indefinite"/>
</circle>
<!-- Valve Stem -->
<rect id="valve-stem" x="18" y="44" width="8" height="66" fill="#94a3b8"/>
</g>
<!-- Valve Body & Gate inside Pipe -->
<path d="M 920 180 L 980 180 L 960 220 L 940 220 Z" fill="#334155" stroke="#475569" stroke-width="2"/>
<rect id="valve-gate" x="942" y="180" width="16" height="20" fill="#0f172a" stroke="#1e293b" stroke-width="1"/>
<!-- 4. Data Buses (Dashed flowing lines) -->
<!-- Sensor to PLC -->
<path d="M 244 452 L 320 452 L 320 220 L 370 220" class="data-bus"/>
<!-- PLC to Valve -->
<path d="M 820 450 L 870 450 L 870 92 L 930 92" class="data-bus" style="stroke:#ff00ea; filter:drop-shadow(0 0 5px #ff00ea);"/>
<!-- 5. PLC Fuzzy Controller Core -->
<g id="plc-system">
<!-- PLC Main Chassis -->
<rect x="370" y="80" width="450" height="520" rx="12" fill="rgba(15, 23, 42, 0.7)" stroke="#00f3ff" stroke-width="2" class="glow-cyan"/>
<!-- Tech corner decors -->
<path d="M 370 100 L 370 80 L 390 80" fill="none" stroke="#fff" stroke-width="3"/>
<path d="M 820 100 L 820 80 L 800 80" fill="none" stroke="#fff" stroke-width="3"/>
<path d="M 370 580 L 370 600 L 390 600" fill="none" stroke="#fff" stroke-width="3"/>
<path d="M 820 580 L 820 600 L 800 600" fill="none" stroke="#fff" stroke-width="3"/>
<text x="595" y="125" fill="#fff" font-size="20" font-weight="bold" text-anchor="middle" letter-spacing="2">内嵌模糊推理引擎 (Fuzzy Logic)</text>
<text x="595" y="145" fill="#94a3b8" font-size="12" text-anchor="middle">二维决策模型 · 自适应去噪与提前动作</text>
<!-- Graph: Error (e) -->
<text x="445" y="185" fill="#00f3ff" font-size="12" font-weight="bold">输入端 1: 液位偏差 e</text>
<use href="#fuzzy-sets-template" x="445" y="195"/>
<g id="e-indicator" transform="translate(0, 0)">
<line x1="595" y1="195" x2="595" y2="235" stroke="#00f3ff" stroke-width="2" class="glow-cyan"/>
<polygon points="595,195 590,185 600,185" fill="#00f3ff"/>
<text id="e-val" x="595" y="178" fill="#00f3ff" font-size="14" font-weight="bold" text-anchor="middle">0.0</text>
</g>
<!-- Graph: Delta Error (de/dt) -->
<text x="445" y="305" fill="#00f3ff" font-size="12" font-weight="bold">输入端 2: 偏差变化率 de/dt (探测浊波)</text>
<use href="#fuzzy-sets-template" x="445" y="315"/>
<g id="de-indicator" transform="translate(0, 0)">
<line x1="595" y1="315" x2="595" y2="355" stroke="#00f3ff" stroke-width="2" class="glow-cyan"/>
<polygon points="595,315 590,305 600,305" fill="#00f3ff"/>
<text id="de-val" x="595" y="298" fill="#00f3ff" font-size="14" font-weight="bold" text-anchor="middle">0.0</text>
</g>
<!-- Graph: Output U -->
<text x="445" y="425" fill="#ff00ea" font-size="12" font-weight="bold">输出端: 连续调节量 U (重心法去模糊)</text>
<use href="#fuzzy-sets-template" x="445" y="435"/>
<g id="u-indicator" transform="translate(0, 0)">
<!-- Defuzzification Area (COG visual) -->
<polygon points="595,435 570,475 620,475" fill="rgba(255, 0, 234, 0.4)" stroke="#ff00ea" stroke-width="1"/>
<line x1="595" y1="435" x2="595" y2="475" stroke="#fff" stroke-width="2" class="glow-magenta"/>
<text id="u-val" x="595" y="423" fill="#ff00ea" font-size="14" font-weight="bold" text-anchor="middle">50.0%</text>
</g>
<!-- Active Rule Display -->
<rect x="400" y="535" width="390" height="42" rx="6" fill="#022c22" stroke="#00ff66" stroke-width="1"/>
<text id="rule-text" x="595" y="561" font-size="14" font-family="monospace" fill="#00ff66" text-anchor="middle" font-weight="bold">ACTIVE RULE: 计算中...</text>
</g>
</svg>
<!-- Interactive Controls Overlay -->
<div class="controls-overlay">
<button id="btn-disturb" class="action-btn">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M2 12h4l3-9 5 18 3-9h5"/></svg>
注入快速浊波 (Surge)
</button>
<div class="slider-box">
<label>目标设定水位 (SV): <span id="lbl-setpoint" style="color:var(--green); font-weight:bold; font-size:15px;">50</span>%</label>
<input type="range" id="slider-setpoint" min="20" max="80" value="50">
</div>
</div>
</div>
<script>
// System Variables
let time = 0;
let level = 50; // Current PV (0-100)
let lastLevel = 50;
let valve = 50; // Actuator output (0-100)
let baseOutflow = 50;
let manualDisturbance = 0;
// Controllers
const sliderSetpoint = document.getElementById('slider-setpoint');
const lblSetpoint = document.getElementById('lbl-setpoint');
const btnDisturb = document.getElementById('btn-disturb');
btnDisturb.addEventListener('click', () => {
// Inject a massive temporary outflow surge simulating a rapid turbid wave
manualDisturbance = 120;
});
// Helper: Clamping
const clamp = (val, min, max) => Math.max(min, Math.min(max, val));
// Helper: Map value to 7-segment fuzzy set name
function getSetName(val, maxVal) {
let r = clamp(val / maxVal, -1, 1);
if(r <= -0.6) return "NB";
if(r <= -0.3) return "NM";
if(r <= -0.1) return "NS";
if(r < 0.1) return "ZO";
if(r < 0.3) return "PS";
if(r < 0.6) return "PM";
return "PB";
}
function updateSimulation() {
time += 0.05;
// 1. Disturbance Physics (Surge decays quickly over time)
manualDisturbance *= 0.94;
let naturalDist = Math.sin(time * 0.4) * 4 + Math.sin(time * 1.3) * 2;
let totalDisturbance = manualDisturbance + naturalDist;
let currentOutflow = baseOutflow + totalDisturbance;
// 2. Liquid Level Physics
let inflow = valve;
let dy = (inflow - currentOutflow) * 0.05;
level += dy;
level = clamp(level, 0, 100);
// 3. Control System (Fuzzy Logic Embedded Simulation)
let setpoint = parseFloat(sliderSetpoint.value);
lblSetpoint.innerText = setpoint;
// Calculate e (Error) and de (Delta Error)
let e = level - setpoint; // range approx -50 to 50
let de = level - lastLevel; // rate of change per tick. Max ~ +/- 5
lastLevel = level;
// Fuzzy rule execution approximation:
// e > 0 (level too high) -> close valve
// de > 0 (level rising fast) -> close valve proactively (This is the IFR core feature!)
let Kp = 1.8;
let Kd = 12.0;
let valveTarget = 50 - (e * Kp) - (de * Kd);
valveTarget = clamp(valveTarget, 0, 100);
// Smooth servo movement continuous output
valve += (valveTarget - valve) * 0.2;
// 4. Update Visual DOM Elements
updateVisuals(setpoint, e, de, totalDisturbance, currentOutflow);
requestAnimationFrame(updateSimulation);
}
function updateVisuals(setpoint, e, de, totalDist, outflow) {
// Tank Water wave generation
let spY = 550 - (setpoint / 100) * 200;
let pvY = 550 - (level / 100) * 200;
let d = `M 102 ${pvY} `;
for(let x = 102; x <= 218; x += 10) {
let wave = Math.sin(x * 0.05 + time * 3) * 3;
// Add turbulence effect during surge
wave += Math.sin(x * 0.2 - time * 5) * (manualDisturbance * 0.05);
d += `L ${x} ${pvY + wave} `;
}
d += `L 218 548 L 102 548 Z`;
document.getElementById('tank-water').setAttribute('d', d);
// Update Levels Text
document.getElementById('sp-line').setAttribute('y1', spY);
document.getElementById('sp-line').setAttribute('y2', spY);
document.getElementById('sp-text').setAttribute('y', spY - 5);
document.getElementById('pv-text').setAttribute('y', pvY - 5);
// Fuzzy Input Indicators
let e_offset = clamp((e / 50) * 150, -150, 150);
let de_offset = clamp((de / 4) * 150, -150, 150);
let u_offset = clamp(((valve - 50) / 50) * 150, -150, 150);
document.getElementById('e-indicator').setAttribute('transform', `translate(${e_offset}, 0)`);
document.getElementById('e-val').textContent = e.toFixed(1);
document.getElementById('de-indicator').setAttribute('transform', `translate(${de_offset}, 0)`);
document.getElementById('de-val').textContent = de.toFixed(2);
document.getElementById('u-indicator').setAttribute('transform', `translate(${u_offset}, 0)`);
document.getElementById('u-val').textContent = valve.toFixed(1) + '%';
// Active Rule Translation
// Note logic inversion for wording: if 'e' is positively huge (PB), valve wants to close heavily (NB)
let e_set = getSetName(e, 50);
let de_set = getSetName(de, 4);
let u_set = getSetName(valve - 50, 50);
document.getElementById('rule-text').textContent = `RULE [激活]: IF e IS ${e_set} AND de/dt IS ${de_set} THEN U IS ${u_set}`;
// Actuator Valve Visuals
let gateHeight = 40 - (valve / 100) * 40;
document.getElementById('valve-gate').setAttribute('height', Math.max(0, gateHeight));
let streamWidth = 30 * (valve / 100);
document.getElementById('in-stream').setAttribute('stroke-width', Math.max(1, streamWidth));
// Outflow Visuals
let outWidth = Math.max(2, (outflow / 100) * 20);
document.getElementById('outflow-stream').setAttribute('stroke-width', outWidth);
const warning = document.getElementById('disturb-warning');
if(manualDisturbance > 20) {
warning.style.opacity = 1;
warning.style.transform = `scale(${1 + Math.random()*0.1})`;
} else {
warning.style.opacity = 0;
}
}
// Start Simulation automatically on load
window.addEventListener('DOMContentLoaded', () => {
requestAnimationFrame(updateSimulation);
});
</script>
</body>
</html>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PLC自适应模糊控制系统 - 最终理想解(IFR)原理演示</title>
<style>
:root {
--bg-base: #050914;
--panel-bg: #0b1221;
--panel-border: #1a2744;
--text-main: #e2e8f0;
--text-dim: #94a3b8;
--accent-cyan: #00f0ff;
--accent-amber: #ff9f1c;
--accent-green: #2ec4b6;
--accent-red: #e71d36;
--font-mono: 'Fira Code', 'Courier New', monospace;
--font-sans: 'PingFang SC', 'Microsoft YaHei', system-ui, sans-serif;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
background-color: var(--bg-base);
color: var(--text-main);
font-family: var(--font-sans);
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
overflow-x: hidden;
background-image:
radial-gradient(circle at 50% 0%, rgba(0, 240, 255, 0.05) 0%, transparent 50%),
linear-gradient(rgba(255,255,255,0.02) 1px, transparent 1px),
linear-gradient(90deg, rgba(255,255,255,0.02) 1px, transparent 1px);
background-size: 100% 100%, 30px 30px, 30px 30px;
}
header {
width: 100%;
padding: 1.5rem 2rem;
text-align: center;
border-bottom: 1px solid var(--panel-border);
background: rgba(11, 18, 33, 0.8);
backdrop-filter: blur(10px);
z-index: 10;
}
h1 {
font-size: 1.5rem;
letter-spacing: 2px;
color: var(--accent-cyan);
text-shadow: 0 0 15px rgba(0, 240, 255, 0.4);
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 0.9rem;
color: var(--text-dim);
max-width: 800px;
margin: 0 auto;
line-height: 1.6;
}
.highlight {
color: var(--accent-amber);
font-weight: bold;
}
.dashboard {
display: grid;
grid-template-columns: 350px 1fr 300px;
gap: 20px;
width: 98vw;
max-width: 1600px;
padding: 20px;
flex: 1;
}
.panel {
background: var(--panel-bg);
border: 1px solid var(--panel-border);
border-radius: 8px;
padding: 20px;
position: relative;
box-shadow: inset 0 0 20px rgba(0,0,0,0.5), 0 8px 16px rgba(0,0,0,0.4);
display: flex;
flex-direction: column;
}
.panel::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0; height: 2px;
background: linear-gradient(90deg, transparent, var(--accent-cyan), transparent);
opacity: 0.5;
}
.panel-title {
font-size: 1rem;
font-weight: 600;
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
color: var(--text-main);
border-bottom: 1px dashed var(--panel-border);
padding-bottom: 10px;
}
.panel-title span {
display: inline-block;
width: 8px;
height: 8px;
background: var(--accent-cyan);
border-radius: 50%;
box-shadow: 0 0 8px var(--accent-cyan);
}
/* SVG & Visualization Styles */
.svg-container {
flex: 1;
width: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
min-height: 250px;
}
svg {
width: 100%;
height: 100%;
overflow: visible;
}
.fuzzy-chart {
display: grid;
grid-template-rows: 1fr 1fr 1fr;
gap: 15px;
height: 100%;
}
.chart-row {
background: rgba(0,0,0,0.2);
border: 1px solid var(--panel-border);
border-radius: 6px;
position: relative;
padding: 10px;
display: flex;
flex-direction: column;
}
.chart-label {
font-size: 0.8rem;
color: var(--text-dim);
font-family: var(--font-mono);
position: absolute;
top: 5px;
left: 10px;
z-index: 2;
}
.chart-value {
position: absolute;
top: 5px;
right: 10px;
font-size: 0.9rem;
font-family: var(--font-mono);
color: var(--accent-cyan);
font-weight: bold;
}
/* Controls */
.controls-group {
margin-bottom: 25px;
}
.control-label {
display: flex;
justify-content: space-between;
font-size: 0.85rem;
margin-bottom: 8px;
color: var(--text-dim);
}
input[type="range"] {
-webkit-appearance: none;
width: 100%;
height: 6px;
background: #1a2744;
border-radius: 3px;
outline: none;
margin-bottom: 10px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 16px;
height: 16px;
border-radius: 50%;
background: var(--accent-cyan);
cursor: pointer;
box-shadow: 0 0 10px var(--accent-cyan);
transition: transform 0.1s;
}
input[type="range"]::-webkit-slider-thumb:hover {
transform: scale(1.2);
}
.btn {
background: transparent;
border: 1px solid var(--accent-cyan);
color: var(--accent-cyan);
padding: 10px 15px;
border-radius: 4px;
cursor: pointer;
font-family: var(--font-sans);
font-size: 0.85rem;
transition: all 0.3s;
width: 100%;
margin-bottom: 10px;
text-align: center;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
}
.btn:hover {
background: rgba(0, 240, 255, 0.1);
box-shadow: inset 0 0 10px rgba(0, 240, 255, 0.2);
}
.btn.active {
background: rgba(0, 240, 255, 0.2);
border-color: var(--accent-cyan);
color: #fff;
text-shadow: 0 0 5px var(--accent-cyan);
}
.btn-amber {
border-color: var(--accent-amber);
color: var(--accent-amber);
}
.btn-amber:hover {
background: rgba(255, 159, 28, 0.1);
box-shadow: inset 0 0 10px rgba(255, 159, 28, 0.2);
}
/* Animation Classes */
.data-flow {
stroke-dasharray: 8;
animation: flow 1s linear infinite;
}
@keyframes flow {
to { stroke-dashoffset: -16; }
}
.glow-pulse {
animation: pulse 2s ease-in-out infinite alternate;
}
@keyframes pulse {
from { filter: drop-shadow(0 0 2px var(--accent-cyan)); }
to { filter: drop-shadow(0 0 12px var(--accent-cyan)); }
}
/* Telemetry overlay */
.telemetry {
position: absolute;
bottom: 20px;
left: 20px;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--accent-green);
pointer-events: none;
opacity: 0.7;
}
.fuzzy-path {
fill: none;
stroke-width: 1.5;
transition: stroke 0.3s, stroke-width 0.3s;
}
.fuzzy-path.active {
stroke-width: 3;
filter: drop-shadow(0 0 6px currentColor);
}
.fuzzy-fill {
transition: fill-opacity 0.2s;
}
</style>
</head>
<body>
<header>
<h1>PLC 自适应模糊控制系统</h1>
<p class="subtitle">
<span class="highlight">最终理想解 (IFR):</span>无需增加外部复杂硬件,仅利用已有 PLC 算力内嵌模糊推理。<br>
当<span style="color:var(--accent-amber)">偏差极小但变化率剧烈(快速浊波)</span>时提前动作,打破“反应迟缓”与“过度震荡”的矛盾,输出连续平滑的调节量。
</p>
</header>
<div class="dashboard">
<!-- Panel 1: Physical Plant -->
<div class="panel">
<div class="panel-title"><span style="background: var(--accent-green); box-shadow: 0 0 8px var(--accent-green);"></span> 被控对象:物理液位系统</div>
<div class="svg-container">
<svg viewBox="0 0 300 400" id="plantSvg">
<defs>
<linearGradient id="waterGrad" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#00f0ff" stop-opacity="0.8" />
<stop offset="100%" stop-color="#0055ff" stop-opacity="0.9" />
</linearGradient>
<filter id="neonGlow">
<feGaussianBlur stdDeviation="4" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
</defs>
<!-- Tank Outline -->
<rect x="50" y="50" width="200" height="250" fill="none" stroke="#2a3655" stroke-width="4" rx="10" />
<!-- Grid inside tank -->
<pattern id="grid" width="20" height="20" patternUnits="userSpaceOnUse">
<path d="M 20 0 L 0 0 0 20" fill="none" stroke="#1a2744" stroke-width="0.5"/>
</pattern>
<rect x="52" y="52" width="196" height="246" fill="url(#grid)" rx="8"/>
<!-- Water Level -->
<rect id="waterLevel" x="52" y="150" width="196" height="148" fill="url(#waterGrad)" rx="0" class="glow-pulse" style="transition: y 0.1s, height 0.1s;" />
<!-- Target Line -->
<line x1="30" y1="150" x2="270" y2="150" stroke="var(--accent-green)" stroke-width="2" stroke-dasharray="5,5" />
<text x="275" y="154" fill="var(--accent-green)" font-size="12" font-family="var(--font-mono)">SP</text>
<!-- Sensors Data Path -->
<path d="M 250 150 L 280 150 L 280 40 L 300 40" fill="none" stroke="var(--accent-cyan)" stroke-width="2" class="data-flow" />
<circle cx="250" cy="150" r="4" fill="var(--accent-cyan)" filter="url(#neonGlow)"/>
<!-- Actuator / Valve -->
<g transform="translate(150, 320)">
<path d="M -30 -10 L -10 10 L 10 -10 L 30 10 L 30 30 L -30 30 Z" fill="#1a2235" stroke="var(--accent-amber)" stroke-width="2" />
<rect x="-5" y="-30" width="10" height="20" fill="#94a3b8" />
<!-- Valve Stem/Handle -->
<g id="valveHandle" style="transition: transform 0.1s;">
<circle cx="0" cy="-35" r="15" fill="none" stroke="var(--accent-amber)" stroke-width="4" />
<line x1="-15" y1="-35" x2="15" y2="-35" stroke="var(--accent-amber)" stroke-width="4" />
</g>
</g>
<!-- Input Pipe Data Flow (Output from PLC) -->
<path d="M 0 360 L 120 360 L 120 320" fill="none" stroke="var(--accent-green)" stroke-width="2" class="data-flow" />
<!-- Telemetry Text -->
<text id="levelText" x="60" y="80" fill="var(--accent-cyan)" font-size="14" font-family="var(--font-mono)">PV: 0.0%</text>
<text id="errorText" x="60" y="100" fill="var(--accent-amber)" font-size="14" font-family="var(--font-mono)">e: 0.0</text>
</svg>
</div>
<div class="telemetry">
[ACTUATOR_STATUS: <span id="actuatorStatus" style="color:var(--accent-amber)">IDLE</span>]
</div>
</div>
<!-- Panel 2: Fuzzy Logic Core -->
<div class="panel">
<div class="panel-title"><span></span> PLC 模糊控制核心 (7段式梯形隶属度)</div>
<div class="fuzzy-chart">
<!-- Input 1: Error (e) -->
<div class="chart-row">
<div class="chart-label">液位偏差 (e) 模糊化</div>
<div class="chart-value" id="valE" style="color: var(--accent-cyan)">0.0</div>
<div class="svg-container">
<svg viewBox="0 0 400 100" preserveAspectRatio="none" id="svgE">
<!-- Generated via JS -->
</svg>
</div>
</div>
<!-- Input 2: Rate of Change (ec) -->
<div class="chart-row">
<div class="chart-label">偏差变化率 (de/dt) 模糊化</div>
<div class="chart-value" id="valEc" style="color: var(--accent-amber)">0.0</div>
<div class="svg-container">
<svg viewBox="0 0 400 100" preserveAspectRatio="none" id="svgEc">
<!-- Generated via JS -->
</svg>
</div>
</div>
<!-- Output: Defuzzification -->
<div class="chart-row">
<div class="chart-label">重心法去模糊 (U) -> 阀门开度</div>
<div class="chart-value" id="valU" style="color: var(--accent-green)">0.0%</div>
<div class="svg-container">
<svg viewBox="0 0 400 100" preserveAspectRatio="none" id="svgU">
<!-- Background Sets -->
<g id="outSets"></g>
<!-- Centroid Polygon -->
<polygon id="centroidShape" points="200,90 190,100 210,100" fill="var(--accent-green)" filter="drop-shadow(0 0 5px var(--accent-green))" style="transition: transform 0.1s" />
<line id="centroidLine" x1="200" y1="10" x2="200" y2="90" stroke="var(--accent-green)" stroke-width="2" stroke-dasharray="4" style="transition: transform 0.1s" />
</svg>
</div>
</div>
</div>
</div>
<!-- Panel 3: Controls & State -->
<div class="panel">
<div class="panel-title"><span style="background: var(--accent-red); box-shadow: 0 0 8px var(--accent-red);"></span> 场景交互验证</div>
<button id="btnAuto" class="btn active">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><polygon points="10 8 16 12 10 16 10 8"/></svg>
自动仿真扰动模式
</button>
<button id="btnScenario" class="btn btn-amber">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2L3 14h9l-1 8 10-12h-9l1-8z"/></svg>
触发 IFR: 快速浊波 (e小, ec大)
</button>
<div style="height: 1px; background: var(--panel-border); margin: 20px 0;"></div>
<div class="controls-group">
<div class="control-label">
<span>手动液位偏差 (e)</span>
<span id="labelE">0</span>
</div>
<input type="range" id="sliderE" min="-100" max="100" value="0">
<div style="display:flex; justify-content:space-between; font-size:0.7rem; color:var(--text-dim); font-family:var(--font-mono)">
<span>-100 (低)</span>
<span>0 (准)</span>
<span>+100 (高)</span>
</div>
</div>
<div class="controls-group">
<div class="control-label">
<span>手动偏差变化率 (de/dt)</span>
<span id="labelEc">0</span>
</div>
<input type="range" id="sliderEc" min="-100" max="100" value="0">
<div style="display:flex; justify-content:space-between; font-size:0.7rem; color:var(--text-dim); font-family:var(--font-mono)">
<span>-100 (速降)</span>
<span>0 (稳)</span>
<span>+100 (速升)</span>
</div>
</div>
<div class="telemetry" style="position: relative; bottom: 0; left: 0; margin-top: auto;">
> SYSTEM LOGS<br>
> RULE_BASE: 49 RULES ACTIVE<br>
> DEFUZZ_METHOD: CENTROID<br>
> INFERENCE: MAX-MIN COMPOSITION
</div>
</div>
</div>
<script>
// System State
let state = {
e: 0, // -100 to 100
ec: 0, // -100 to 100
u: 50, // 0 to 100 (Valve Openness)
autoMode: true,
targetLevel: 150,
currentLevel: 150, // Physical Y coord in SVG (lower Y means higher level conceptually, but let's map standard math)
// Mathematical mapping: Level 0-100% maps to Y 250 to 50
};
const fuzzySets = [
{ name: 'NB', color: '#e71d36', points: [0, 0, 0, 16.6] },
{ name: 'NM', color: '#ff9f1c', points: [0, 16.6, 33.3] },
{ name: 'NS', color: '#ffbf69', points: [16.6, 33.3, 50] },
{ name: 'ZE', color: '#2ec4b6', points: [33.3, 50, 66.6] },
{ name: 'PS', color: '#00f0ff', points: [50, 66.6, 83.3] },
{ name: 'PM', color: '#0077b6', points: [66.6, 83.3, 100] },
{ name: 'PB', color: '#023e8a', points: [83.3, 100, 100, 100] }
];
// Init Fuzzy SVG Charts
function initFuzzySVG(svgId) {
const svg = document.getElementById(svgId);
const width = 400;
const height = 100;
const padding = 15;
const drawH = height - padding * 2;
// Draw axis
svg.innerHTML = `<line x1="0" y1="${height - padding}" x2="${width}" y2="${height - padding}" stroke="#1a2744" stroke-width="2" />`;
fuzzySets.forEach((set, index) => {
let d = "";
if(index === 0) {
// Trapezoid left
const p1x = 0; const p2x = (16.6/100)*width;
d = `M 0 ${height - padding} L 0 ${padding} L ${p2x} ${height - padding}`;
} else if (index === 6) {
// Trapezoid right
const p1x = (83.3/100)*width;
d = `M ${p1x} ${height - padding} L ${width} ${padding} L ${width} ${height - padding}`;
} else {
// Triangle
const p1x = (set.points[0]/100)*width;
const p2x = (set.points[1]/100)*width;
const p3x = (set.points[2]/100)*width;
d = `M ${p1x} ${height - padding} L ${p2x} ${padding} L ${p3x} ${height - padding}`;
}
svg.innerHTML += `
<path id="${svgId}_path_${index}" d="${d}" class="fuzzy-path" stroke="${set.color}" stroke-opacity="0.5" />
<path id="${svgId}_fill_${index}" d="${d} Z" class="fuzzy-fill" fill="${set.color}" fill-opacity="0" />
<text x="${(index/6)*width}" y="${padding - 2}" fill="${set.color}" font-size="10" font-family="var(--font-mono)" text-anchor="middle" opacity="0.6">${set.name}</text>
`;
});
// Add dynamic indicator line
svg.innerHTML += `<line id="${svgId}_cursor" x1="200" y1="0" x2="200" y2="${height}" stroke="white" stroke-width="2" opacity="0.8" />`;
svg.innerHTML += `<circle id="${svgId}_dot" cx="200" cy="${height - padding}" r="4" fill="white" />`;
}
// Fuzzy Logic Approximation
function calculateFuzzy(e, ec) {
// Simplified Rule Base for UI Simulation
// Returns U (0 to 100)
// e: -100 (low water) to +100 (high water)
// ec: -100 (dropping fast) to +100 (rising fast)
// PD equivalent control logic
// U = Kp*e + Kd*ec
// If water is low (e < 0) and dropping (ec < 0), open valve (U = 100)
// If water is high (e > 0) and rising (ec > 0), close valve (U = 0)
let u_raw = -0.5 * e - 0.5 * ec; // gives -100 to 100
// Map to 0-100%
let u = (u_raw + 100) / 2;
// Add non-linear fuzzy characteristic (S-curve approximation)
// This emphasizes the IFR: acts strongly in middle-small deviation with high rate
u = (Math.sin((u / 100 - 0.5) * Math.PI) + 1) * 50;
return Math.max(0, Math.min(100, u));
}
function getMembershipIndices(val) {
// val is -100 to 100. Map to 0-100 for percentage
let p = (val + 100) / 2;
let active = [];
fuzzySets.forEach((set, i) => {
let center = (i / 6) * 100;
let dist = Math.abs(p - center);
if(dist < 16.6) active.push(i); // Simplified membership check
});
if(active.length === 0) {
active.push(Math.round((p/100)*6));
}
return active;
}
function updateVisuals() {
// Calculate U
state.u = calculateFuzzy(state.e, state.ec);
// Update DOM Elements
document.getElementById('sliderE').value = state.e.toFixed(0);
document.getElementById('sliderEc').value = state.ec.toFixed(0);
document.getElementById('labelE').innerText = state.e.toFixed(1);
document.getElementById('labelEc').innerText = state.ec.toFixed(1);
document.getElementById('valE').innerText = state.e.toFixed(1);
document.getElementById('valEc').innerText = state.ec.toFixed(1);
document.getElementById('valU').innerText = state.u.toFixed(1) + '%';
// 1. Update Tank
// Y mapped from 250(e= -100) to 50(e=100).
// wait, e = Target(150) - current.
// If e = 0, current = 150.
// If e = -100 (level too low, actual physical Y is high e.g. 250).
let actualY = 150 - state.e; // simplistic mapping
actualY = Math.max(50, Math.min(250, actualY));
let waterHeight = 250 - actualY + 50; // base bottom is y=300
document.getElementById('waterLevel').setAttribute('y', actualY);
document.getElementById('waterLevel').setAttribute('height', waterHeight);
// Update Valve Handle Rotation (0 to 100% -> 0 to 90 degrees)
document.getElementById('valveHandle').style.transform = `rotate(${state.u * 0.9}deg)`;
document.getElementById('actuatorStatus').innerText = state.u < 10 ? 'CLOSED' : (state.u > 90 ? 'FULL OPEN' : 'REGULATING');
document.getElementById('levelText').innerText = `PV: ${( (300-actualY)/2.5 ).toFixed(1)}%`;
document.getElementById('errorText').innerText = `e: ${state.e.toFixed(1)}`;
// 2. Update Fuzzy Charts E & EC
function updateChart(svgId, val) {
let p = (val + 100) / 2; // 0-100
let x = (p / 100) * 400;
document.getElementById(`${svgId}_cursor`).setAttribute('x1', x);
document.getElementById(`${svgId}_cursor`).setAttribute('x2', x);
document.getElementById(`${svgId}_dot`).setAttribute('cx', x);
let activeIndices = getMembershipIndices(val);
for(let i=0; i<7; i++) {
let path = document.getElementById(`${svgId}_path_${i}`);
let fill = document.getElementById(`${svgId}_fill_${i}`);
if(activeIndices.includes(i)) {
path.classList.add('active');
path.setAttribute('stroke-opacity', '1');
fill.setAttribute('fill-opacity', '0.4');
} else {
path.classList.remove('active');
path.setAttribute('stroke-opacity', '0.3');
fill.setAttribute('fill-opacity', '0');
}
}
}
updateChart('svgE', state.e);
updateChart('svgEc', state.ec);
// 3. Update Centroid Output Chart
let uX = (state.u / 100) * 400;
// Translate the centroid shape
let dx = uX - 200; // 200 is base center
document.getElementById('centroidShape').style.transform = `translateX(${dx}px)`;
document.getElementById('centroidLine').style.transform = `translateX(${dx}px)`;
// Highlight output sets based on U
let outActive = getMembershipIndices((state.u / 100 * 200) - 100);
for(let i=0; i<7; i++) {
let path = document.getElementById(`svgU_path_${i}`);
let fill = document.getElementById(`svgU_fill_${i}`);
if(path) {
if(outActive.includes(i)) {
path.setAttribute('stroke-opacity', '1');
fill.setAttribute('fill-opacity', '0.3');
} else {
path.setAttribute('stroke-opacity', '0.2');
fill.setAttribute('fill-opacity', '0');
}
}
}
}
// Simulation Loop
let time = 0;
function loop() {
if (state.autoMode) {
time += 0.02;
// Generate a disturbance: Sine wave + noise
let disturbance = Math.sin(time) * 40 + Math.sin(time * 3.5) * 20;
// Simulate physical system response (very abstracted)
// Valve opening (U) fills the tank, disturbance drains it or adds to it
let flowRate = (state.u - 50) * 0.5; // u=50 is equilibrium
let prevE = state.e;
// Target is 0 error. e = (disturbance - flow effect)
// In a real simulation, we integrate. Let's do a simple dynamic model:
// Let physical level = current level + flow + disturbance_delta
// For pure visual effect of the IFR:
// We smoothly guide e towards the disturbance
state.e = disturbance;
// Calculate rate of change accurately based on simulation step
let rawEc = (state.e - prevE) * 50;
state.ec = state.ec * 0.8 + rawEc * 0.2; // Smooth the rate of change
// Clamp
state.e = Math.max(-100, Math.min(100, state.e));
state.ec = Math.max(-100, Math.min(100, state.ec));
}
updateVisuals();
requestAnimationFrame(loop);
}
// Event Listeners
window.addEventListener('DOMContentLoaded', () => {
initFuzzySVG('svgE');
initFuzzySVG('svgEc');
initFuzzySVG('svgU'); // Out is 0-100 mapped over the 7 sets
// Start Loop
requestAnimationFrame(loop);
// Controls
const sliderE = document.getElementById('sliderE');
const sliderEc = document.getElementById('sliderEc');
const btnAuto = document.getElementById('btnAuto');
const btnScenario = document.getElementById('btnScenario');
sliderE.addEventListener('input', (e) => {
state.autoMode = false;
btnAuto.classList.remove('active');
state.e = parseFloat(e.target.value);
});
sliderEc.addEventListener('input', (e) => {
state.autoMode = false;
btnAuto.classList.remove('active');
state.ec = parseFloat(e.target.value);
});
btnAuto.addEventListener('click', () => {
state.autoMode = true;
btnAuto.classList.add('active');
time = 0; // reset phase
});
btnScenario.addEventListener('click', () => {
state.autoMode = false;
btnAuto.classList.remove('active');
// IFR Scenario: Error is small (near 0), but it's dropping extremely fast (turbulent wave)
state.e = 5; // Small error
state.ec = -90; // High rate of change (dropping)
// Animate sliders to this position
sliderE.value = state.e;
sliderEc.value = state.ec;
});
});
</script>
</body>
</html>
积分规则:每轮对话扣10分
等待动画代码生成...
