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();
})();