File size: 19,749 Bytes
95411eb 984c1cf 95411eb 8f2de14 95411eb 8f2de14 95411eb 8f2de14 c1bf61e 95411eb c1bf61e 95411eb c1bf61e 8f2de14 95411eb c57e940 95411eb c1bf61e 95411eb c57e940 95411eb c57e940 95411eb c57e940 95411eb c57e940 95411eb | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 | (() => {
'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 = `<span class="path-index">${String(index + 1).padStart(2, '0')}</span><span class="path-copy"><strong>${item.label}</strong><small>${item.scope}</small></span><span class="split ${item.split}">${item.split === 'validation' ? 'VAL' : item.split.toUpperCase()}</span>`;
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'
? '<strong>B · 未知硬化闭合</strong><span>隐藏三通道 Chaboche 硬化;DENIM、Incomplete J2 与 GRU 按同一协议比较。</span>'
: '<strong>A · 已知方程基准</strong><span>完整 J2 / Chaboche 方程与参数已知;比较模型对未见加载路径的预测。</span>';
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 = `<div class="ladder-rank">${String(groupIndex).padStart(2, '0')}</div><div class="ladder-model">${name}</div><div class="ladder-protocol"><span class="protocol-pill ${protocol === 'closure' ? 'closure' : ''}">${protocolName}</span></div><div class="bar-track"><div class="bar-fill ${kind}" style="width:${width.toFixed(2)}%"></div></div><div class="ladder-value">${value < .1 ? value.toFixed(4) : value.toFixed(3)} MPa<small>${comparison}</small></div>`;
ladder.append(line);
});
}
const modelAtlas = [
{
name: 'Pointwise MLP', short: '单点映射', family: '数据驱动 · 无记忆', badge: '当前量 → 当前量',
tagline: '只看当前应变,不读取加载历史。',
nodes: [['输入', '当前应变 εₜ', '单个时刻'], ['神经映射', 'MLP', '黑箱回归', 'learned'], ['输出', '当前应力 σₜ', '无内部状态']],
summary: '结构最简单、推理很快,但同一应变在不同加载历史下可能对应不同应力,单点映射无法区分。',
traits: ['无', '无', '预测全部应力', '弱']
},
{
name: 'GRU', short: '时序黑箱', family: '数据驱动 · 隐式记忆', badge: '历史序列 → 应力',
tagline: '从应变序列中压缩出隐藏历史状态。',
nodes: [['输入', '应变历史 ε₀:ₜ', '完整序列'], ['序列编码', 'GRU', '隐状态记忆', 'learned'], ['输出', '应力序列 σ₀:ₜ', '端到端预测']],
summary: '能够识别滞回和反向加载,但记忆完全藏在网络隐状态中;路径分布变化时,外推稳定性受训练数据覆盖范围影响。',
traits: ['隐状态', '无', '学习全部演化', '中—弱']
},
{
name: 'Physics-state GRU', short: '弱物理时序', family: '物理增强 · 隐状态', badge: '物理特征 + 时序网络',
tagline: '把物理状态作为特征交给循环网络。',
nodes: [['输入', '应变 + 物理特征', '状态提示', 'physics'], ['状态更新', 'Physics-state GRU', '学习时序演化', 'learned'], ['输出', '应力 + 隐状态', '弱物理约束']],
summary: '物理特征改善了网络的输入表达,但积分更新仍主要由黑箱时序网络承担,守恒与一致性通常需要额外约束。',
traits: ['隐状态', '物理特征', '学习状态更新', '中']
},
{
name: 'Physics-integrator NN', short: '神经辅助积分器', family: '已知方程 · 神经辅助积分', badge: '完整本构方程已知',
tagline: '网络修正塑性增量初值,完整方程完成一致性校正。',
nodes: [['输入', 'Δε + zₙ', '增量与状态'], ['力学初值', 'Δλ seed', '解析近似', 'physics'], ['神经修正', 'NN correction', '改善初值', 'learned'], ['方程校正', '返回映射', '屈服一致性', 'physics'], ['输出', 'σₙ₊₁ + zₙ₊₁', '高精度']],
summary: '网络不学习新的材料规律;J2 / Chaboche 方程、参数和内部变量均已知。它只帮助求解塑性增量,并由完整方程完成最终校正。',
traits: ['显式内部变量', '完整方程', '修正塑性增量初值', '强']
},
{
name: 'Incomplete J2', short: '不完备基线', family: '不完备物理 · 无神经闭合', badge: '骨架正确 · 演化缺失',
tagline: '保留基本塑性框架,但硬化演化表达不足。',
nodes: [['输入', 'Δε + zₙ', '增量与状态'], ['力学骨架', '弹性 + J2 屈服', '基本约束', 'physics'], ['缺失环节', '不完备硬化', '系统偏差'], ['输出', 'σₙ₊₁', '误差累积']],
summary: '物理骨架提供基本合理性,但缺少真实材料的复杂记忆与硬化通道,循环和非比例路径下会产生持续误差。',
traits: ['有限', '基础骨架', '无', '中—弱']
},
{
name: 'DENIM', short: '离散能量神经内部变量模型', family: '不完备物理 · 神经闭合', badge: '力学骨架 + 可学习演化',
tagline: '保留可验证力学积分,以神经内部变量补全未知演化。',
nodes: [['输入', 'Δε + zₙ', '增量与历史状态'], ['力学骨架', '弹性 · J2 · 流动法则', '显式约束', 'physics'], ['神经闭合', '快—慢记忆通道', '未知硬化演化', 'learned memory'], ['隐式积分', '返回映射', '一致性求解', 'physics'], ['输出', 'σₙ₊₁ + zₙ₊₁', '可追踪状态']],
summary: '网络不替代整套本构方程,只学习物理骨架未描述的演化项;因此同时保留路径记忆、求解约束和内部状态可解释性。',
traits: ['显式神经内部变量', '核心骨架', '闭合未知演化', '强']
},
{
name: 'Causal TCN', short: '因果时序卷积', family: '数据驱动 · 有限历史窗口', badge: '只读当前与过去',
tagline: '通过多层因果卷积,提取不同时间尺度的加载历史。',
summary: '膨胀卷积扩大历史感受野,不读取未来输入;记忆由有限历史窗口提供,而非循环隐状态。',
traits: ['历史感受野', '无', '预测全部应力', '依赖训练覆盖']
}
];
function buildModelSelector() {
const selector = $('modelSelector');
selector.innerHTML = '';
[0, 1, 6, 2, 3, 4, 5].forEach(index => {
const model = modelAtlas[index];
const button = document.createElement('button');
button.type = 'button';
button.dataset.modelIndex = index;
button.className = `model-select${index === 5 ? ' active' : ''}`;
button.setAttribute('aria-pressed', String(index === 5));
button.innerHTML = `<strong>${model.name}</strong><small>${model.short}</small>`;
button.onclick = () => renderArchitecture(index);
selector.append(button);
});
}
function renderArchitecture(index) {
const model = modelAtlas[index];
document.querySelectorAll('.model-select').forEach((button, buttonIndex) => {
const active = Number(button.dataset.modelIndex) === index;
button.classList.toggle('active', active);
button.setAttribute('aria-pressed', String(active));
});
$('architectureFamily').textContent = model.family;
$('architectureName').textContent = model.name;
$('architectureTagline').textContent = model.tagline;
$('architectureBadge').textContent = model.badge;
$('architectureSummary').textContent = model.summary;
['traitMemory', 'traitPhysics', 'traitLearning', 'traitOod'].forEach((id, traitIndex) => { $(id).textContent = model.traits[traitIndex]; });
window.DenimArchitecture.render(index);
}
document.querySelectorAll('[data-series]').forEach(input => {
input.addEventListener('change', () => { state.visible[input.dataset.series] = input.checked; drawHysteresis(); });
});
$('timeline').addEventListener('input', event => { state.step = Number(event.target.value); render(); });
$('play').onclick = togglePlayback;
$('memoryPair').onclick = () => {
const pair = trajectory().memory_pair;
state.step = pair[state.pairToggle++ % 2];
render();
};
$('reset').onclick = () => { stopPlayback(); state.step = 0; state.pairToggle = 0; render(); };
document.addEventListener('keydown', event => {
if ($('labView').hidden) return;
if (event.target.matches('input, button, a')) return;
if (event.code === 'Space') { event.preventDefault(); togglePlayback(); }
if (event.key === 'ArrowRight') { stopPlayback(); state.step = Math.min(state.step + 1, trajectory().strain_pct.length - 1); render(); }
if (event.key === 'ArrowLeft') { stopPlayback(); state.step = Math.max(state.step - 1, 0); render(); }
if (event.key.toLowerCase() === 'm') { $('memoryPair').click(); }
});
document.querySelectorAll('[data-view]').forEach(button => {
button.onclick = () => {
const target = button.dataset.view;
const evidence = target === 'evidence';
const models = target === 'models';
$('labView').hidden = evidence || models;
$('evidenceView').hidden = !evidence;
$('modelsView').hidden = !models;
document.querySelectorAll('[data-view]').forEach(value => {
value.classList.toggle('active', value === button);
value.setAttribute('aria-selected', String(value === button));
});
if (target !== 'lab') stopPlayback();
if (target === 'lab') requestAnimationFrame(render);
};
});
let resizeTimer;
window.addEventListener('resize', () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(render, 90); });
buildPathList();
buildLadder();
buildModelSelector();
renderArchitecture(5);
render();
})();
|