分享图
动画工坊
引擎就绪
<!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分