(() => { 'use strict'; const data = window.DENIM_DEMO; if (!data) throw new Error('DENIM demo data not loaded'); const $ = id => document.getElementById(id); const state = { trajectory: 6, step: 120, playing: false, pairToggle: 0, visible: { truth: true, denim: true, gru: false } }; const colors = { truth: '#f2f0ea', denim: '#d38a51', gru: '#6d91e8', cyan: '#72d6d1' }; let timer = null; function trajectory() { return data.trajectories[state.trajectory]; } function extent(values) { let min = Math.min(...values), max = Math.max(...values); if (Math.abs(max - min) < 1e-9) { min -= 1; max += 1; } const pad = (max - min) * .1; return [min - pad, max + pad]; } const map = (value, a, b, c, d) => c + (value - a) * (d - c) / (b - a); const pathString = points => points.map((p, i) => `${i ? 'L' : 'M'}${p[0].toFixed(2)},${p[1].toFixed(2)}`).join(' '); const svgElement = (name, attrs = {}) => { const element = document.createElementNS('http://www.w3.org/2000/svg', name); for (const [key, value] of Object.entries(attrs)) element.setAttribute(key, value); return element; }; function append(parent, name, attrs = {}, text = '') { const element = svgElement(name, attrs); if (text) element.textContent = text; parent.append(element); return element; } function buildPathList() { const list = $('pathList'); list.innerHTML = ''; data.trajectories.forEach((item, index) => { const button = document.createElement('button'); button.type = 'button'; button.className = `path-button${index === state.trajectory ? ' active' : ''}`; button.innerHTML = `${String(index + 1).padStart(2, '0')}${item.label}${item.scope}${item.split === 'validation' ? 'VAL' : item.split.toUpperCase()}`; button.onclick = () => { // Keep the user's transport state; the existing timer reads the current trajectory. if (state.trajectory === index) return; state.trajectory = index; state.step = 0; state.pairToggle = 0; $('timeline').value = 0; buildPathList(); render(); }; list.append(button); }); } function chartFrame(svg, xValues, yValues, labels) { const width = Math.max(280, svg.clientWidth || 700); const height = Math.max(160, svg.clientHeight || 430); svg.setAttribute('viewBox', `0 0 ${width} ${height}`); svg.innerHTML = ''; const margin = { left: 58, right: 20, top: 18, bottom: 43 }; const [xmin, xmax] = extent(xValues), [ymin, ymax] = extent(yValues); const sx = value => map(value, xmin, xmax, margin.left, width - margin.right); const sy = value => map(value, ymin, ymax, height - margin.bottom, margin.top); for (let i = 0; i <= 5; i++) { const x = map(i, 0, 5, margin.left, width - margin.right); const y = map(i, 0, 5, margin.top, height - margin.bottom); append(svg, 'line', { x1: x, x2: x, y1: margin.top, y2: height - margin.bottom, class: 'grid' }); append(svg, 'line', { x1: margin.left, x2: width - margin.right, y1: y, y2: y, class: 'grid' }); append(svg, 'text', { x, y: height - 21, 'text-anchor': 'middle' }, (xmin + i * (xmax - xmin) / 5).toFixed(2)); append(svg, 'text', { x: margin.left - 9, y: y + 3, 'text-anchor': 'end' }, (ymax - i * (ymax - ymin) / 5).toFixed(Math.max(Math.abs(ymin), Math.abs(ymax)) < 2 ? 2 : 0)); } append(svg, 'line', { x1: margin.left, x2: width - margin.right, y1: height - margin.bottom, y2: height - margin.bottom, class: 'axis' }); append(svg, 'line', { x1: margin.left, x2: margin.left, y1: margin.top, y2: height - margin.bottom, class: 'axis' }); append(svg, 'text', { x: (margin.left + width - margin.right) / 2, y: height - 4, 'text-anchor': 'middle' }, labels.x); append(svg, 'text', { x: 13, y: (margin.top + height - margin.bottom) / 2, transform: `rotate(-90 13 ${(margin.top + height - margin.bottom) / 2})`, 'text-anchor': 'middle' }, labels.y); return { sx, sy, width, height, margin }; } function drawHysteresis() { const item = trajectory(), svg = $('hysteresisChart'); const allStress = [...item.truth_mpa, ...item.denim_mpa, ...item.gru_mpa]; const frame = chartFrame(svg, item.strain_pct, allStress, { x: `应变 ${item.primary} / %`, y: `应力 ${item.primary} / MPa` }); const definitions = [ ['truth', item.truth_mpa], ['denim', item.denim_mpa], ['gru', item.gru_mpa] ]; for (const [name, values] of definitions) { if (!state.visible[name]) continue; const points = item.strain_pct.map((x, i) => [frame.sx(x), frame.sy(values[i])]); append(svg, 'path', { d: pathString(points), class: `series ${name}-path future` }); append(svg, 'path', { d: pathString(points.slice(0, state.step + 1)), class: `series ${name}-path` }); append(svg, 'circle', { cx: points[state.step][0], cy: points[state.step][1], r: name === 'denim' ? 4.5 : 3.5, class: 'marker', stroke: colors[name] }); } const pair = item.memory_pair; pair.forEach((index, n) => { const x = frame.sx(item.strain_pct[index]); const y = frame.sy(item.truth_mpa[index]); append(svg, 'circle', { cx: x, cy: y, r: 7, fill: 'none', stroke: n ? colors.denim : colors.cyan, 'stroke-width': 1.2, opacity: .7 }); }); } function drawPathPlane() { const item = trajectory(), svg = $('pathChart'); const frame = chartFrame(svg, item.path_x_pct, item.path_y_pct, { x: `${item.primary} / %`, y: `${item.secondary} / %` }); const points = item.path_x_pct.map((x, i) => [frame.sx(x), frame.sy(item.path_y_pct[i])]); append(svg, 'path', { d: pathString(points), class: 'series path-plane future' }); append(svg, 'path', { d: pathString(points.slice(0, state.step + 1)), class: 'series path-plane' }); append(svg, 'circle', { cx: points[state.step][0], cy: points[state.step][1], r: 4.5, class: 'marker', stroke: colors.cyan }); } function updateState() { const item = trajectory(), i = state.step; $('stepLabel').textContent = `${i + 1} / ${item.strain_pct.length}`; $('timeline').max = item.strain_pct.length - 1; $('timeline').value = i; $('pathName').textContent = item.label; $('pathScope').textContent = item.scope; $('component').textContent = `${item.primary.toUpperCase()} / ${item.secondary.toUpperCase()}`; $('currentError').textContent = `${item.step_error_mpa[i].toFixed(3)} MPa`; $('yieldResidual').textContent = `${item.yield_residual_pa[i].toFixed(1)} Pa`; $('stressNow').textContent = item.denim_mpa[i].toFixed(1); $('memoryOne').textContent = `${item.memory_1_mpa[i].toFixed(1)} MPa`; $('memoryTwo').textContent = `${item.memory_2_mpa[i].toFixed(1)} MPa`; $('isotropicNow').textContent = `${item.isotropic_mpa[i].toFixed(1)} MPa`; $('peeqNow').textContent = item.peeq[i].toFixed(5); $('pathAxes').textContent = `${item.primary.toUpperCase()}—${item.secondary.toUpperCase()} 应变平面`; const plastic = i > 0 && item.peeq[i] - item.peeq[i - 1] > 1e-10; $('stateMode').textContent = plastic ? '塑性演化' : '弹性响应'; document.querySelector('.state-dot i').style.background = plastic ? 'var(--copper)' : 'var(--green)'; const maxA = Math.max(...item.memory_1_mpa, 1), maxB = Math.max(...item.memory_2_mpa, 1); $('memoryA').style.transform = `rotate(${360 * item.memory_1_mpa[i] / maxA - 25}deg)`; $('memoryB').style.transform = `rotate(${-320 * item.memory_2_mpa[i] / maxB + 45}deg)`; const pair = item.memory_pair; const closeToPair = Math.abs(i - pair[0]) < 2 || Math.abs(i - pair[1]) < 2; $('historyBadge').textContent = closeToPair ? '同应变 · 异历史' : (plastic ? '塑性状态演化' : '历史状态保持'); } function render() { updateState(); drawHysteresis(); drawPathPlane(); } function stopPlayback() { state.playing = false; clearInterval(timer); $('play').textContent = '播放路径'; $('play').setAttribute('aria-pressed', 'false'); } function togglePlayback() { state.playing = !state.playing; $('play').textContent = state.playing ? '暂停播放' : '播放路径'; $('play').setAttribute('aria-pressed', String(state.playing)); clearInterval(timer); if (state.playing) timer = setInterval(() => { const count = trajectory().strain_pct.length; state.step = (state.step + 2) % count; render(); }, 55); } function buildLadder() { const full = data.summary.full_physics_path_ood; const closure = data.summary.test; const rows = [ ['Pointwise MLP', 'full', full.pointwise_mlp.rmse_mpa, 'blackbox'], ['GRU', 'full', full.gru.rmse_mpa, 'blackbox'], ['LSTM', 'full', full.lstm.rmse_mpa, 'blackbox'], ['Causal TCN', 'full', full.causal_tcn.rmse_mpa, 'blackbox'], ['Physics-state GRU', 'full', full.physics_state_gru.rmse_mpa, 'blackbox'], ['Physics-integrator NN', 'full', full.physics_integrator_nn.rmse_mpa, 'physics'], ['Incomplete J2', 'closure', closure.incomplete_j2.rmse_mpa, 'closure'], ['GRU', 'closure', closure.gru.rmse_mpa, 'closure'], ['DENIM', 'closure', closure.denrm.rmse_mpa, 'denim'], ].sort((a, b) => (a[1] === b[1] ? a[2]-b[2] : a[1]==='closure' ? -1 : 1)); const ladder = $('ladder'); ladder.innerHTML = ''; const maxValue = Math.max(...rows.map(row => row[2])) * 1.04; const denimValue = closure.denrm.rmse_mpa; let groupIndex = 0; rows.forEach((row, index) => { if (index === 0 || rows[index - 1][1] !== row[1]) groupIndex = 0; groupIndex++; if (index === 0 || rows[index - 1][1] !== row[1]) { const heading = document.createElement('div'); heading.className = 'protocol-heading'; heading.innerHTML = row[1] === 'closure' ? 'B · 未知硬化闭合隐藏三通道 Chaboche 硬化;DENIM、Incomplete J2 与 GRU 按同一协议比较。' : 'A · 已知方程基准完整 J2 / Chaboche 方程与参数已知;比较模型对未见加载路径的预测。'; ladder.append(heading); } const [name, protocol, value, kind] = row; const line = document.createElement('div'); line.className = `ladder-group${name === 'DENIM' ? ' featured' : ''}`; const width = 100 * value / maxValue; let protocolName = protocol === 'full' ? '协议 A · 已知方程 · 新路径' : '协议 B · 未知硬化 · 新路径'; if (name === 'Physics-integrator NN') protocolName = '协议 A · 完整方程积分'; const ratio = value / denimValue; const comparison = protocol === 'full' ? '仅在协议 A 内比较' : name === 'DENIM' ? '协议 B 基准' : (ratio >= 1 ? `误差比 DENIM 高 ${ratio.toFixed(ratio >= 10 ? 0 : 1)}×` : `误差比 DENIM 低 ${(1 / ratio).toFixed(0)}×`); line.innerHTML = `