Download app.js from HaomingLuo/AgentFEM-DENIM-Material-Memory-Lab: direct link, hf CLI and curl.
- Browser
- Download file 19.7 kB
-
https://huggingface.co/spaces/HaomingLuo/AgentFEM-DENIM-Material-Memory-Lab/resolve/main/app.js
- Command line
-
hf download hf://spaces/HaomingLuo/AgentFEM-DENIM-Material-Memory-Lab/app.js
-
curl -L -o app.js https://huggingface.co/spaces/HaomingLuo/AgentFEM-DENIM-Material-Memory-Lab/resolve/main/app.js
19.7 kB
| (() => { | |
| '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(); | |
| })(); | |