shrut27 commited on
Commit
3b213e4
·
verified ·
1 Parent(s): 5b9dd53

Upload folder using huggingface_hub

Browse files
Files changed (3) hide show
  1. README.md +14 -13
  2. data/dashboard_payload.json +0 -0
  3. index.html +945 -59
README.md CHANGED
@@ -10,25 +10,26 @@ short_description: AI crash material ranking and CAE card generation
10
 
11
  # Crash Intelligence Platform
12
 
13
- AI-powered Streamlit application (browser runtime via stlite) for recommending, predicting, and validating automotive crash-performance materials across steel, aluminum, magnesium, composites, polymers, foams, elastomers, and adhesives.
14
 
15
- ## Features
16
 
17
- - **Material Data Layer** — searchable database of crash-relevant mechanical, cost, and sustainability properties
18
- - **Crash Scenario Intelligence** — frontal, side, rear, pole, pedestrian, battery pack, BIW, and EV underbody scenarios
19
- - **AI Ranking Engine** — multi-objective ranking for crashworthiness, weight, cost, sustainability, and failure risk
20
- - **Material Comparison** — radar charts, scatter trade-offs, and family-level benchmarks
21
- - **CAE Material Card Generator** — draft LS-DYNA / Abaqus / PAM-CRASH / Radioss cards with plastic curves
22
- - **Validation Workflow** — AI vs CAE vs physical proxies aligned to Euro NCAP / FMVSS / IIHS
 
23
 
24
- ## Run locally (full Streamlit server)
 
 
 
 
25
 
26
  ```bash
27
  pip install -r requirements.txt
28
  python -m utils.data_generator
29
  streamlit run app.py
30
  ```
31
-
32
- ## Hugging Face Space
33
-
34
- This Space hosts the Streamlit app through a static/stlite runtime so the interactive dashboard is publicly available without paid Docker hardware.
 
10
 
11
  # Crash Intelligence Platform
12
 
13
+ Comprehensive automotive crash-material intelligence dashboard for recommending, ranking, and validating materials across steel, aluminum, magnesium, composites, polymers, foams, elastomers, and adhesives.
14
 
15
+ ## Dashboard modules
16
 
17
+ 1. **Overview** — KPI gauges, family crashworthiness, crash vs weight scatter, scenario×family heatmap
18
+ 2. **Material Explorer** — energy absorption, cost vs sustainability, stress–strain curves, property table
19
+ 3. **Crash Scenario AI** — top recommendations by scenario/component with intrusion–energy trade-offs
20
+ 4. **Compare & Rank** — multi-objective radar + weighted ranking
21
+ 5. **Material Cards** — draft LS-DYNA / Abaqus / PAM-CRASH / Radioss cards with download
22
+ 6. **Validation** — AI vs CAE parity, error distribution, certification ladder
23
+ 7. **Data Library** — browse/export dataset views
24
 
25
+ ## Live Space
26
+
27
+ https://huggingface.co/spaces/WellmatixGenAI/Crash-Intelligence
28
+
29
+ ## Local Streamlit (optional)
30
 
31
  ```bash
32
  pip install -r requirements.txt
33
  python -m utils.data_generator
34
  streamlit run app.py
35
  ```
 
 
 
 
data/dashboard_payload.json ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -1,67 +1,953 @@
1
  <!DOCTYPE html>
2
  <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <title>Crash Intelligence Platform</title>
7
- <link
8
- rel="stylesheet"
9
- href="https://cdn.jsdelivr.net/npm/@stlite/browser@0.80.5/build/stlite.css"
10
- />
11
- <style>
12
- html, body, #root {
13
- margin: 0;
14
- padding: 0;
15
- width: 100%;
16
- height: 100%;
17
- background: #ffffff;
18
- color: #0f172a;
19
- }
20
- .boot {
21
- font-family: "Source Sans 3", "Segoe UI", sans-serif;
22
- color: #0B3D2E;
23
- display: flex;
24
- align-items: center;
25
- justify-content: center;
26
- height: 100%;
27
- font-size: 1.1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  }
29
- </style>
30
- <script type="module">
31
- import { mount } from "https://cdn.jsdelivr.net/npm/@stlite/browser@0.80.5/build/stlite.js";
32
 
33
- const root = document.getElementById("root");
34
- root.innerHTML = '<div class="boot">Loading Crash Intelligence Platform…</div>';
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- mount(
37
- {
38
- entrypoint: "app.py",
39
- requirements: ["pandas", "numpy", "plotly"],
40
- streamlitConfig: {
41
- "client.toolbarMode": "minimal",
42
- "theme.base": "light",
43
- "theme.primaryColor": "#0B6E4F",
44
- "theme.backgroundColor": "#ffffff",
45
- "theme.secondaryBackgroundColor": "#f8fafc",
46
- "theme.textColor": "#0f172a",
47
- },
48
- files: {
49
- "app.py": { url: "./app.py" },
50
- "utils/__init__.py": { url: "./utils/__init__.py" },
51
- "utils/data_generator.py": { url: "./utils/data_generator.py" },
52
- "utils/calculations.py": { url: "./utils/calculations.py" },
53
- "utils/visualizations.py": { url: "./utils/visualizations.py" },
54
- "data/materials.csv": { url: "./data/materials.csv" },
55
- "data/recommendations.csv": { url: "./data/recommendations.csv" },
56
- "data/stress_strain.csv": { url: "./data/stress_strain.csv" },
57
- "data/validation.csv": { url: "./data/validation.csv" },
58
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  },
60
- root
61
- );
62
- </script>
63
- </head>
64
- <body>
65
- <div id="root"></div>
66
- </body>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
67
  </html>
 
1
  <!DOCTYPE html>
2
  <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Crash Intelligence Platform</title>
7
+ <script src="https://cdn.plot.ly/plotly-2.35.2.min.js"></script>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@500;600;700&family=Source+Sans+3:wght@400;600;700&display=swap" rel="stylesheet" />
11
+ <style>
12
+ :root {
13
+ --ink: #0f172a;
14
+ --muted: #475569;
15
+ --green: #0b6e4f;
16
+ --green-dark: #0b3d2e;
17
+ --blue: #1b4965;
18
+ --sky: #5fa8d3;
19
+ --bg: #f1f5f9;
20
+ --card: #ffffff;
21
+ --line: #e2e8f0;
22
+ --danger: #c44536;
23
+ }
24
+ * { box-sizing: border-box; }
25
+ html, body {
26
+ margin: 0;
27
+ padding: 0;
28
+ background: var(--bg);
29
+ color: var(--ink);
30
+ font-family: "Source Sans 3", "Segoe UI", sans-serif;
31
+ }
32
+ .layout {
33
+ display: grid;
34
+ grid-template-columns: 280px 1fr;
35
+ min-height: 100vh;
36
+ }
37
+ .sidebar {
38
+ background: linear-gradient(180deg, #0b3d2e 0%, #1b4965 100%);
39
+ color: #f8fafc;
40
+ padding: 1.25rem 1rem 2rem;
41
+ position: sticky;
42
+ top: 0;
43
+ height: 100vh;
44
+ overflow-y: auto;
45
+ }
46
+ .sidebar h2 {
47
+ font-family: "IBM Plex Sans", sans-serif;
48
+ font-size: 1.05rem;
49
+ margin: 0 0 0.75rem;
50
+ color: #fff;
51
+ }
52
+ .sidebar label {
53
+ display: block;
54
+ font-size: 0.82rem;
55
+ margin: 0.75rem 0 0.3rem;
56
+ color: #e2e8f0;
57
+ }
58
+ .sidebar select,
59
+ .sidebar input[type="range"] {
60
+ width: 100%;
61
+ }
62
+ .sidebar select {
63
+ background: rgba(255,255,255,0.12);
64
+ color: #fff;
65
+ border: 1px solid rgba(255,255,255,0.25);
66
+ border-radius: 8px;
67
+ padding: 0.45rem 0.5rem;
68
+ }
69
+ .sidebar select option { color: #0f172a; }
70
+ .family-box {
71
+ max-height: 180px;
72
+ overflow-y: auto;
73
+ background: rgba(255,255,255,0.08);
74
+ border-radius: 8px;
75
+ padding: 0.4rem 0.55rem;
76
+ border: 1px solid rgba(255,255,255,0.15);
77
+ }
78
+ .family-box label {
79
+ display: flex;
80
+ gap: 0.4rem;
81
+ align-items: center;
82
+ margin: 0.25rem 0;
83
+ font-size: 0.78rem;
84
+ color: #f1f5f9;
85
+ }
86
+ .weight-val { float: right; color: #bae6fd; }
87
+ .caption {
88
+ margin-top: 1rem;
89
+ font-size: 0.75rem;
90
+ color: #cbd5e1;
91
+ line-height: 1.4;
92
+ }
93
+ .main { padding: 1.1rem 1.4rem 2rem; }
94
+ .hero {
95
+ background: linear-gradient(120deg, #0b3d2e 0%, #1b4965 55%, #5fa8d3 100%);
96
+ color: #fff;
97
+ border-radius: 14px;
98
+ padding: 1.35rem 1.5rem;
99
+ margin-bottom: 1rem;
100
+ }
101
+ .hero h1 {
102
+ font-family: "IBM Plex Sans", sans-serif;
103
+ margin: 0 0 0.35rem;
104
+ font-size: 1.85rem;
105
+ color: #fff;
106
+ }
107
+ .hero p { margin: 0; color: #e2e8f0; max-width: 900px; line-height: 1.45; }
108
+ .tabs {
109
+ display: flex;
110
+ flex-wrap: wrap;
111
+ gap: 0.4rem;
112
+ margin: 0.8rem 0 1rem;
113
+ }
114
+ .tab-btn {
115
+ background: #fff;
116
+ border: 1px solid var(--line);
117
+ color: var(--ink);
118
+ border-radius: 999px;
119
+ padding: 0.45rem 0.9rem;
120
+ font-weight: 600;
121
+ cursor: pointer;
122
+ font-size: 0.9rem;
123
+ }
124
+ .tab-btn.active {
125
+ background: var(--green);
126
+ border-color: var(--green);
127
+ color: #fff;
128
+ }
129
+ .panel { display: none; }
130
+ .panel.active { display: block; }
131
+ .metrics {
132
+ display: grid;
133
+ grid-template-columns: repeat(5, minmax(0, 1fr));
134
+ gap: 0.75rem;
135
+ margin-bottom: 1rem;
136
+ }
137
+ .metric {
138
+ background: var(--card);
139
+ border: 1px solid var(--line);
140
+ border-left: 4px solid var(--green);
141
+ border-radius: 10px;
142
+ padding: 0.85rem 1rem;
143
+ }
144
+ .metric .label { color: var(--muted); font-size: 0.82rem; }
145
+ .metric .value {
146
+ color: var(--green);
147
+ font-size: 1.45rem;
148
+ font-weight: 700;
149
+ margin-top: 0.15rem;
150
+ font-family: "IBM Plex Sans", sans-serif;
151
+ }
152
+ .grid-2 {
153
+ display: grid;
154
+ grid-template-columns: 1fr 1fr;
155
+ gap: 0.9rem;
156
+ margin-bottom: 0.9rem;
157
+ }
158
+ .grid-3 {
159
+ display: grid;
160
+ grid-template-columns: 1fr 1fr 1fr;
161
+ gap: 0.9rem;
162
+ margin-bottom: 0.9rem;
163
+ }
164
+ .card {
165
+ background: var(--card);
166
+ border: 1px solid var(--line);
167
+ border-radius: 12px;
168
+ padding: 0.75rem;
169
+ min-height: 120px;
170
+ }
171
+ .card h3 {
172
+ margin: 0 0 0.5rem;
173
+ font-size: 1rem;
174
+ color: var(--green-dark);
175
+ font-family: "IBM Plex Sans", sans-serif;
176
+ }
177
+ .note {
178
+ background: #fff;
179
+ border: 1px solid var(--line);
180
+ border-left: 4px solid var(--green);
181
+ border-radius: 10px;
182
+ padding: 0.9rem 1rem;
183
+ color: var(--ink);
184
+ margin: 0.8rem 0;
185
+ line-height: 1.45;
186
+ }
187
+ table.data {
188
+ width: 100%;
189
+ border-collapse: collapse;
190
+ font-size: 0.85rem;
191
+ background: #fff;
192
+ }
193
+ table.data th, table.data td {
194
+ border-bottom: 1px solid var(--line);
195
+ padding: 0.45rem 0.5rem;
196
+ text-align: left;
197
+ color: var(--ink);
198
+ }
199
+ table.data th {
200
+ background: #ecfdf5;
201
+ color: var(--green-dark);
202
+ position: sticky;
203
+ top: 0;
204
+ }
205
+ .table-wrap {
206
+ max-height: 360px;
207
+ overflow: auto;
208
+ border: 1px solid var(--line);
209
+ border-radius: 10px;
210
+ background: #fff;
211
+ }
212
+ .controls-row {
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ gap: 0.75rem;
216
+ align-items: end;
217
+ margin-bottom: 0.8rem;
218
+ }
219
+ .controls-row label {
220
+ font-size: 0.82rem;
221
+ color: var(--muted);
222
+ display: block;
223
+ margin-bottom: 0.25rem;
224
+ }
225
+ .controls-row select, .controls-row button, .controls-row input {
226
+ padding: 0.45rem 0.6rem;
227
+ border-radius: 8px;
228
+ border: 1px solid var(--line);
229
+ background: #fff;
230
+ color: var(--ink);
231
+ }
232
+ .controls-row button {
233
+ background: var(--green);
234
+ color: #fff;
235
+ border: none;
236
+ font-weight: 600;
237
+ cursor: pointer;
238
+ }
239
+ pre.card-text {
240
+ background: #0f172a;
241
+ color: #e2e8f0;
242
+ padding: 1rem;
243
+ border-radius: 10px;
244
+ overflow: auto;
245
+ font-size: 0.82rem;
246
+ line-height: 1.4;
247
+ min-height: 320px;
248
+ }
249
+ .loading {
250
+ min-height: 60vh;
251
+ display: flex;
252
+ align-items: center;
253
+ justify-content: center;
254
+ color: var(--green-dark);
255
+ font-size: 1.1rem;
256
+ }
257
+ .footer {
258
+ margin-top: 1.2rem;
259
+ color: var(--muted);
260
+ font-size: 0.8rem;
261
+ border-top: 1px solid var(--line);
262
+ padding-top: 0.8rem;
263
+ }
264
+ @media (max-width: 1100px) {
265
+ .layout { grid-template-columns: 1fr; }
266
+ .sidebar { position: relative; height: auto; }
267
+ .metrics { grid-template-columns: repeat(2, 1fr); }
268
+ .grid-2, .grid-3 { grid-template-columns: 1fr; }
269
+ }
270
+ </style>
271
+ </head>
272
+ <body>
273
+ <div id="app" class="loading">Loading Crash Intelligence comprehensive dashboard…</div>
274
+
275
+ <script>
276
+ const COLORS = ["#0B6E4F","#08A045","#1B4965","#5FA8D3","#C44536","#E8871E","#6B4C9A","#2A9D8F","#E76F51","#264653","#F4A261","#457B9D","#9B2226","#005F73","#CA6702"];
277
+ const LAYOUT = {
278
+ paper_bgcolor: "#ffffff",
279
+ plot_bgcolor: "#f8fafc",
280
+ font: { family: "Source Sans 3, Segoe UI, sans-serif", color: "#0f172a", size: 12 },
281
+ margin: { l: 55, r: 25, t: 45, b: 50 },
282
+ legend: { bgcolor: "rgba(255,255,255,0.92)", bordercolor: "#ddd", borderwidth: 1 }
283
+ };
284
+
285
+ let DATA = null;
286
+ let state = {
287
+ families: [],
288
+ scenario: "Side Impact",
289
+ component: "Battery Enclosure",
290
+ maxCost: 40,
291
+ minUts: 200,
292
+ maxDensity: 8,
293
+ weights: { crash: 0.3, weight: 0.2, cost: 0.2, sustain: 0.15, failure: 0.15 },
294
+ tab: "overview"
295
+ };
296
+
297
+ function num(v, d=1) { return Number(v).toFixed(d); }
298
+ function pct(v) { return `${(100 * v).toFixed(0)}%`; }
299
+
300
+ function filteredTable() {
301
+ return DATA.table.filter(r =>
302
+ state.families.includes(r.family) &&
303
+ r.cost_usd_kg <= state.maxCost &&
304
+ r.uts_mpa >= state.minUts &&
305
+ r.density_g_cm3 <= state.maxDensity
306
+ );
307
+ }
308
+
309
+ function filteredScatter() {
310
+ return DATA.scatter.filter(r =>
311
+ state.families.includes(r.family) &&
312
+ r.cost_usd_kg <= state.maxCost &&
313
+ r.uts_mpa >= state.minUts &&
314
+ r.density_g_cm3 <= state.maxDensity
315
+ );
316
+ }
317
+
318
+ function familySummary() {
319
+ return DATA.family_summary.filter(r => state.families.includes(r.family));
320
+ }
321
+
322
+ function moScore(r) {
323
+ const w = state.weights;
324
+ const tot = w.crash + w.weight + w.cost + w.sustain + w.failure || 1;
325
+ return (
326
+ (w.crash / tot) * r.crashworthiness_index +
327
+ (w.weight / tot) * r.lightweighting_score +
328
+ (w.cost / tot) * Math.min(r.cost_performance_score * 1.2, 100) +
329
+ (w.sustain / tot) * r.sustainability_score +
330
+ (w.failure / tot) * (100 * (1 - r.failure_risk))
331
+ );
332
+ }
333
+
334
+ function rankedMaterials(n = 8) {
335
+ return [...filteredTable()]
336
+ .map(r => ({ ...r, mo_score: moScore(r) }))
337
+ .sort((a, b) => b.mo_score - a.mo_score)
338
+ .slice(0, n);
339
+ }
340
+
341
+ function scenarioRecs() {
342
+ const key = `${state.scenario}||${state.component}`;
343
+ let rows = (DATA.recommendations[key] || []).filter(r => state.families.includes(r.family));
344
+ if (!rows.length) {
345
+ rows = rankedMaterials(5).map(r => ({
346
+ material_name: r.material_name,
347
+ family: r.family,
348
+ joining_method: "Hybrid Weld-Bond",
349
+ crash_score: r.crashworthiness_index,
350
+ energy_absorption_kj: r.energy_absorption_potential * 2,
351
+ intrusion_mm: 40,
352
+ peak_force_kn: r.uts_mpa * 0.03,
353
+ crush_force_efficiency: 0.65,
354
+ weight_reduction_pct: r.lightweighting_score * 0.4,
355
+ cost_score: r.cost_performance_score,
356
+ sustainability_score: r.sustainability_score,
357
+ simulation_risk: r.failure_risk * 100,
358
+ thickness_mm: 2.0
359
+ }));
360
  }
361
+ return rows.slice(0, 5);
362
+ }
 
363
 
364
+ function plotBarH(div, rows, xKey, yKey, title, colorScale = false) {
365
+ const sorted = [...rows].sort((a, b) => a[xKey] - b[xKey]);
366
+ const trace = {
367
+ type: "bar",
368
+ orientation: "h",
369
+ x: sorted.map(r => r[xKey]),
370
+ y: sorted.map(r => r[yKey]),
371
+ marker: colorScale
372
+ ? { color: sorted.map(r => r[xKey]), colorscale: [[0, "#D8F3DC"], [1, "#0B6E4F"]], showscale: false }
373
+ : { color: "#0B6E4F" },
374
+ hovertemplate: `%{y}<br>${xKey}: %{x:.2f}<extra></extra>`
375
+ };
376
+ Plotly.newPlot(div, [trace], { ...LAYOUT, title, height: 430 }, { responsive: true, displayModeBar: false });
377
+ }
378
 
379
+ function plotScatter(div, rows, x, y, color, size, title) {
380
+ const families = [...new Set(rows.map(r => r[color]))];
381
+ const traces = families.map((f, i) => {
382
+ const sub = rows.filter(r => r[color] === f);
383
+ return {
384
+ type: "scatter",
385
+ mode: "markers",
386
+ name: f,
387
+ x: sub.map(r => r[x]),
388
+ y: sub.map(r => r[y]),
389
+ text: sub.map(r => r.material_name || ""),
390
+ marker: {
391
+ size: sub.map(r => size ? Math.max(7, Math.min(22, (r[size] || 10) / 80)) : 9),
392
+ color: COLORS[i % COLORS.length],
393
+ opacity: 0.75,
394
+ line: { width: 0.5, color: "#fff" }
 
 
 
 
 
 
395
  },
396
+ hovertemplate: "%{text}<br>%{xaxis.title.text}: %{x:.1f}<br>%{yaxis.title.text}: %{y:.1f}<extra>" + f + "</extra>"
397
+ };
398
+ });
399
+ Plotly.newPlot(div, traces, {
400
+ ...LAYOUT,
401
+ title,
402
+ height: 430,
403
+ xaxis: { title: x.replaceAll("_", " ") },
404
+ yaxis: { title: y.replaceAll("_", " ") }
405
+ }, { responsive: true, displayModeBar: false });
406
+ }
407
+
408
+ function plotRadar(div, rows) {
409
+ const cats = ["Crash", "Weight", "Cost-Perf", "Sustainability", "Energy Abs."];
410
+ const traces = rows.slice(0, 5).map((r, i) => {
411
+ const vals = [
412
+ r.crashworthiness_index,
413
+ r.lightweighting_score,
414
+ Math.min(r.cost_performance_score * 1.5, 100),
415
+ r.sustainability_score,
416
+ Math.min(r.energy_absorption_potential * 2, 100)
417
+ ];
418
+ vals.push(vals[0]);
419
+ return {
420
+ type: "scatterpolar",
421
+ r: vals,
422
+ theta: [...cats, cats[0]],
423
+ fill: "toself",
424
+ name: r.material_name,
425
+ opacity: 0.55,
426
+ line: { color: COLORS[i % COLORS.length], width: 2 }
427
+ };
428
+ });
429
+ Plotly.newPlot(div, traces, {
430
+ ...LAYOUT,
431
+ title: "Multi-Objective Material Comparison",
432
+ height: 460,
433
+ polar: {
434
+ bgcolor: "#f8fafc",
435
+ radialaxis: { visible: true, range: [0, 100], gridcolor: "#e5e7eb" },
436
+ angularaxis: { gridcolor: "#e5e7eb" }
437
+ }
438
+ }, { responsive: true, displayModeBar: false });
439
+ }
440
+
441
+ function plotHeatmap(div) {
442
+ const rows = DATA.heatmap.filter(r => state.families.includes(r.family));
443
+ const scenarios = [...new Set(rows.map(r => r.crash_scenario))];
444
+ const families = [...new Set(rows.map(r => r.family))];
445
+ const z = scenarios.map(s => families.map(f => {
446
+ const hit = rows.find(r => r.crash_scenario === s && r.family === f);
447
+ return hit ? hit.crash_score : null;
448
+ }));
449
+ Plotly.newPlot(div, [{
450
+ type: "heatmap",
451
+ z, x: families, y: scenarios,
452
+ colorscale: [[0, "#F1FAEE"], [0.5, "#1B4965"], [1, "#0B6E4F"]],
453
+ hoverongaps: false,
454
+ colorbar: { title: "Crash Score" }
455
+ }], { ...LAYOUT, title: "Avg Crash Score by Scenario × Family", height: 520 }, { responsive: true, displayModeBar: false });
456
+ }
457
+
458
+ function plotGauge(div, value, title, color) {
459
+ Plotly.newPlot(div, [{
460
+ type: "indicator",
461
+ mode: "gauge+number",
462
+ value,
463
+ title: { text: title, font: { size: 14, color: "#1a1a1a" } },
464
+ number: { font: { color: "#1a1a1a" } },
465
+ gauge: {
466
+ axis: { range: [0, 100] },
467
+ bar: { color },
468
+ bgcolor: "#f1f5f9",
469
+ bordercolor: "#cbd5e1",
470
+ steps: [
471
+ { range: [0, 40], color: "#fee2e2" },
472
+ { range: [40, 70], color: "#fef3c7" },
473
+ { range: [70, 100], color: "#dcfce7" }
474
+ ]
475
+ }
476
+ }], { height: 220, margin: { t: 40, b: 10, l: 20, r: 20 }, paper_bgcolor: "#fff", font: { color: "#1a1a1a" } }, { responsive: true, displayModeBar: false });
477
+ }
478
+
479
+ function plotCurves(div, ids) {
480
+ const traces = [];
481
+ ids.forEach((id, i) => {
482
+ const c = DATA.curves[id];
483
+ if (!c) return;
484
+ traces.push({
485
+ type: "scatter", mode: "lines", name: `${c.material_name} (QS)`,
486
+ x: c.strain, y: c.stress_mpa,
487
+ line: { color: COLORS[i % COLORS.length], width: 2.5 }
488
+ });
489
+ traces.push({
490
+ type: "scatter", mode: "lines", name: `${c.material_name} (HR)`,
491
+ x: c.strain, y: c.stress_high_rate_mpa,
492
+ line: { color: COLORS[i % COLORS.length], width: 2, dash: "dash" }
493
+ });
494
+ });
495
+ Plotly.newPlot(div, traces, {
496
+ ...LAYOUT,
497
+ title: "Stress–Strain Curves (Strain-Rate Sensitive)",
498
+ height: 440,
499
+ xaxis: { title: "True Strain" },
500
+ yaxis: { title: "True Stress (MPa)" }
501
+ }, { responsive: true, displayModeBar: false });
502
+ }
503
+
504
+ function plotValidation(divParity, divHist) {
505
+ const rows = DATA.validation.filter(r => state.families.includes(r.family));
506
+ Plotly.newPlot(divParity, [
507
+ {
508
+ type: "scatter", mode: "markers", name: "AI vs CAE",
509
+ x: rows.map(r => r.cae_crash_score),
510
+ y: rows.map(r => r.ai_crash_score),
511
+ marker: { color: "#1B4965", size: 7, opacity: 0.55 }
512
  },
513
+ {
514
+ type: "scatter", mode: "lines", name: "Ideal",
515
+ x: [0, 100], y: [0, 100],
516
+ line: { color: "#C44536", dash: "dash", width: 2 }
517
+ }
518
+ ], { ...LAYOUT, title: "AI Prediction vs CAE Validation", height: 420, xaxis: { title: "CAE Crash Score" }, yaxis: { title: "AI Crash Score" } }, { responsive: true, displayModeBar: false });
519
+
520
+ const pass = rows.filter(r => r.pass_fail === "Pass").map(r => r.ai_cae_error_pct);
521
+ const review = rows.filter(r => r.pass_fail !== "Pass").map(r => r.ai_cae_error_pct);
522
+ Plotly.newPlot(divHist, [
523
+ { type: "histogram", x: pass, name: "Pass", marker: { color: "#0B6E4F" }, opacity: 0.85 },
524
+ { type: "histogram", x: review, name: "Review", marker: { color: "#C44536" }, opacity: 0.85 }
525
+ ], { ...LAYOUT, barmode: "overlay", title: "AI–CAE Error Distribution", height: 420, xaxis: { title: "AI–CAE Error (%)" } }, { responsive: true, displayModeBar: false });
526
+ }
527
+
528
+ function materialCard(mat, solver) {
529
+ const n = 10;
530
+ const strains = Array.from({ length: n }, (_, i) => i * mat.failure_strain / (n - 1));
531
+ const stresses = strains.map((eps, i) => {
532
+ const t = i / (n - 1);
533
+ return mat.yield_strength_mpa + (mat.uts_mpa - mat.yield_strength_mpa) * t;
534
+ });
535
+ const cardType = mat.material_card_type || "MAT_024";
536
+ const pr = (mat.family || "").includes("Aluminum") || mat.family === "Magnesium" ? 0.30 : 0.29;
537
+ let text = `*KEYWORD (${solver} draft)\n`;
538
+ text += `$ Material: ${mat.material_name} (${mat.family})\n`;
539
+ text += `$ Card type: ${cardType}\n`;
540
+ text += `$ Confidence: ${num(mat.confidence_score, 2)}\n`;
541
+ text += `$ Validation: Draft — public-data prototype\n`;
542
+ text += `*MAT_PIECEWISE_LINEAR_PLASTICITY\n`;
543
+ text += `$ RO (g/cm3) = ${mat.density_g_cm3}\n`;
544
+ text += `$ E (GPa) = ${mat.youngs_modulus_gpa}\n`;
545
+ text += `$ PR = ${pr}\n`;
546
+ text += `$ SIGY (MPa) = ${mat.yield_strength_mpa}\n`;
547
+ text += `$ FAIL = ${mat.failure_strain}\n`;
548
+ text += `$ C (strain-rate) = ${mat.strain_rate_sensitivity}\n`;
549
+ text += `$ Plastic curve (strain, stress MPa):\n`;
550
+ strains.forEach((e, i) => { text += `$ ${e.toFixed(5)}, ${stresses[i].toFixed(2)}\n`; });
551
+ text += `$ Damage: Linear softening to zero stress at failure strain\n`;
552
+ text += `$ Temperature: Room-temperature card; scale factors TBD\n*END`;
553
+ return { text, strains, stresses, cardType };
554
+ }
555
+
556
+ function tableHTML(rows, cols) {
557
+ if (!rows.length) return "<p>No rows match the current filters.</p>";
558
+ const head = cols.map(c => `<th>${c.label}</th>`).join("");
559
+ const body = rows.map(r => `<tr>${cols.map(c => `<td>${typeof r[c.key] === "number" ? num(r[c.key], c.digits ?? 2) : (r[c.key] ?? "")}</td>`).join("")}</tr>`).join("");
560
+ return `<div class="table-wrap"><table class="data"><thead><tr>${head}</tr></thead><tbody>${body}</tbody></table></div>`;
561
+ }
562
+
563
+ function renderShell() {
564
+ const families = DATA.meta.families;
565
+ if (!state.families.length) state.families = families.slice();
566
+
567
+ document.getElementById("app").className = "";
568
+ document.getElementById("app").innerHTML = `
569
+ <div class="layout">
570
+ <aside class="sidebar">
571
+ <h2>Filters & Targets</h2>
572
+ <label>Material families</label>
573
+ <div class="family-box" id="familyBox"></div>
574
+ <label>Crash scenario</label>
575
+ <select id="scenario"></select>
576
+ <label>Vehicle component</label>
577
+ <select id="component"></select>
578
+ <label>Max cost (USD/kg): <span id="maxCostVal">${state.maxCost}</span></label>
579
+ <input type="range" id="maxCost" min="1" max="80" step="1" value="${state.maxCost}" />
580
+ <label>Min UTS (MPa): <span id="minUtsVal">${state.minUts}</span></label>
581
+ <input type="range" id="minUts" min="20" max="1800" step="20" value="${state.minUts}" />
582
+ <label>Max density (g/cm³): <span id="maxDensityVal">${state.maxDensity}</span></label>
583
+ <input type="range" id="maxDensity" min="0.1" max="8" step="0.1" value="${state.maxDensity}" />
584
+ <h2 style="margin-top:1.2rem">Multi-objective weights</h2>
585
+ <label>Crash <span class="weight-val" id="wCrashVal">${state.weights.crash.toFixed(2)}</span></label>
586
+ <input type="range" id="wCrash" min="0" max="1" step="0.05" value="${state.weights.crash}" />
587
+ <label>Lightweighting <span class="weight-val" id="wWeightVal">${state.weights.weight.toFixed(2)}</span></label>
588
+ <input type="range" id="wWeight" min="0" max="1" step="0.05" value="${state.weights.weight}" />
589
+ <label>Cost <span class="weight-val" id="wCostVal">${state.weights.cost.toFixed(2)}</span></label>
590
+ <input type="range" id="wCost" min="0" max="1" step="0.05" value="${state.weights.cost}" />
591
+ <label>Sustainability <span class="weight-val" id="wSustVal">${state.weights.sustain.toFixed(2)}</span></label>
592
+ <input type="range" id="wSust" min="0" max="1" step="0.05" value="${state.weights.sustain}" />
593
+ <label>Low failure risk <span class="weight-val" id="wFailVal">${state.weights.failure.toFixed(2)}</span></label>
594
+ <input type="range" id="wFail" min="0" max="1" step="0.05" value="${state.weights.failure}" />
595
+ <p class="caption" id="dbCaption"></p>
596
+ </aside>
597
+ <main class="main">
598
+ <div class="hero">
599
+ <h1>Crash Intelligence Platform</h1>
600
+ <p>AI-powered material recommendation, prediction, and CAE card generation for automotive crash-performance across steel, aluminum, magnesium, composites, polymers, foams, elastomers, and adhesives.</p>
601
+ </div>
602
+ <div class="tabs" id="tabs">
603
+ <button class="tab-btn" data-tab="overview">Overview</button>
604
+ <button class="tab-btn" data-tab="explorer">Material Explorer</button>
605
+ <button class="tab-btn" data-tab="scenario">Crash Scenario AI</button>
606
+ <button class="tab-btn" data-tab="compare">Compare & Rank</button>
607
+ <button class="tab-btn" data-tab="cards">Material Cards</button>
608
+ <button class="tab-btn" data-tab="validation">Validation</button>
609
+ <button class="tab-btn" data-tab="library">Data Library</button>
610
+ </div>
611
+ <div id="content"></div>
612
+ <div class="footer">Crash Intelligence Platform · Public-style material & crash databases · Calibrate with supplier cards and full-vehicle CAE for OEM production use.</div>
613
+ </main>
614
+ </div>`;
615
+
616
+ const famBox = document.getElementById("familyBox");
617
+ famBox.innerHTML = families.map(f => `
618
+ <label><input type="checkbox" value="${f}" ${state.families.includes(f) ? "checked" : ""}/> ${f}</label>
619
+ `).join("");
620
+
621
+ const scen = document.getElementById("scenario");
622
+ scen.innerHTML = DATA.meta.scenarios.map(s => `<option ${s === state.scenario ? "selected" : ""}>${s}</option>`).join("");
623
+ const comp = document.getElementById("component");
624
+ comp.innerHTML = DATA.meta.components.map(s => `<option ${s === state.component ? "selected" : ""}>${s}</option>`).join("");
625
+
626
+ document.getElementById("dbCaption").textContent =
627
+ `Database: ${DATA.meta.n_materials.toLocaleString()} materials · ${DATA.meta.n_recommendations.toLocaleString()} scenario predictions · ${DATA.meta.n_validation.toLocaleString()} validation pairs`;
628
+
629
+ bindControls();
630
+ setTab(state.tab);
631
+ }
632
+
633
+ function bindControls() {
634
+ document.querySelectorAll("#familyBox input").forEach(cb => {
635
+ cb.addEventListener("change", () => {
636
+ state.families = [...document.querySelectorAll("#familyBox input:checked")].map(x => x.value);
637
+ if (!state.families.length) state.families = DATA.meta.families.slice();
638
+ renderActive();
639
+ });
640
+ });
641
+ const map = [
642
+ ["scenario", v => state.scenario = v],
643
+ ["component", v => state.component = v],
644
+ ];
645
+ map.forEach(([id, fn]) => document.getElementById(id).addEventListener("change", e => { fn(e.target.value); renderActive(); }));
646
+
647
+ const ranges = [
648
+ ["maxCost", v => { state.maxCost = +v; document.getElementById("maxCostVal").textContent = v; }],
649
+ ["minUts", v => { state.minUts = +v; document.getElementById("minUtsVal").textContent = v; }],
650
+ ["maxDensity", v => { state.maxDensity = +v; document.getElementById("maxDensityVal").textContent = v; }],
651
+ ["wCrash", v => { state.weights.crash = +v; document.getElementById("wCrashVal").textContent = (+v).toFixed(2); }],
652
+ ["wWeight", v => { state.weights.weight = +v; document.getElementById("wWeightVal").textContent = (+v).toFixed(2); }],
653
+ ["wCost", v => { state.weights.cost = +v; document.getElementById("wCostVal").textContent = (+v).toFixed(2); }],
654
+ ["wSust", v => { state.weights.sustain = +v; document.getElementById("wSustVal").textContent = (+v).toFixed(2); }],
655
+ ["wFail", v => { state.weights.failure = +v; document.getElementById("wFailVal").textContent = (+v).toFixed(2); }],
656
+ ];
657
+ ranges.forEach(([id, fn]) => {
658
+ const el = document.getElementById(id);
659
+ el.addEventListener("input", e => fn(e.target.value));
660
+ el.addEventListener("change", () => renderActive());
661
+ });
662
+
663
+ document.querySelectorAll(".tab-btn").forEach(btn => {
664
+ btn.addEventListener("click", () => setTab(btn.dataset.tab));
665
+ });
666
+ }
667
+
668
+ function setTab(tab) {
669
+ state.tab = tab;
670
+ document.querySelectorAll(".tab-btn").forEach(b => b.classList.toggle("active", b.dataset.tab === tab));
671
+ renderActive();
672
+ }
673
+
674
+ function renderActive() {
675
+ const root = document.getElementById("content");
676
+ const filt = filteredTable();
677
+ const avg = (key) => filt.length ? filt.reduce((s, r) => s + r[key], 0) / filt.length : 0;
678
+
679
+ if (state.tab === "overview") {
680
+ root.innerHTML = `
681
+ <div class="metrics">
682
+ <div class="metric"><div class="label">Materials</div><div class="value">${filt.length.toLocaleString()}</div></div>
683
+ <div class="metric"><div class="label">Avg Crash Index</div><div class="value">${num(avg("crashworthiness_index"))}</div></div>
684
+ <div class="metric"><div class="label">Avg Energy Potential</div><div class="value">${num(avg("energy_absorption_potential"), 2)}</div></div>
685
+ <div class="metric"><div class="label">Avg Sustainability</div><div class="value">${num(avg("sustainability_score"))}</div></div>
686
+ <div class="metric"><div class="label">AI–CAE Pass Rate</div><div class="value">${pct(DATA.meta.pass_rate)}</div></div>
687
+ </div>
688
+ <div class="grid-3">
689
+ <div class="card"><div id="g1"></div></div>
690
+ <div class="card"><div id="g2"></div></div>
691
+ <div class="card"><div id="g3"></div></div>
692
+ </div>
693
+ <div class="grid-2">
694
+ <div class="card"><div id="p1"></div></div>
695
+ <div class="card"><div id="p2"></div></div>
696
+ </div>
697
+ <div class="card"><div id="p3"></div></div>
698
+ <div class="note"><strong>Value proposition:</strong> Shortlist safer, lighter, cheaper, and more sustainable crash-critical materials before expensive CAE and physical testing — then export draft solver-ready material cards for LS-DYNA, Abaqus, PAM-CRASH, and Radioss.</div>
699
+ `;
700
+ plotGauge("g1", avg("crashworthiness_index"), "Crashworthiness", "#0B6E4F");
701
+ plotGauge("g2", avg("lightweighting_score"), "Lightweighting", "#1B4965");
702
+ plotGauge("g3", avg("sustainability_score"), "Sustainability", "#2A9D8F");
703
+ plotBarH("p1", familySummary(), "crash", "family", "Crashworthiness by Family", true);
704
+ plotScatter("p2", filteredScatter(), "lightweighting_score", "crashworthiness_index", "family", "uts_mpa", "Crashworthiness vs Lightweighting");
705
+ plotHeatmap("p3");
706
+ return;
707
+ }
708
+
709
+ if (state.tab === "explorer") {
710
+ const curveIds = Object.keys(DATA.curves).filter(id => state.families.includes(DATA.curves[id].family)).slice(0, 4);
711
+ root.innerHTML = `
712
+ <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Material Data Explorer</h3>
713
+ <p style="color:var(--muted)">Browse standardized mechanical, cost, and sustainability properties across automotive crash materials.</p>
714
+ <div class="grid-2">
715
+ <div class="card"><div id="e1"></div></div>
716
+ <div class="card"><div id="e2"></div></div>
717
+ </div>
718
+ <div class="card"><div id="e3"></div></div>
719
+ ${tableHTML(filt.slice(0, 120), [
720
+ {key:"material_name", label:"Material"},
721
+ {key:"family", label:"Family"},
722
+ {key:"density_g_cm3", label:"Density"},
723
+ {key:"youngs_modulus_gpa", label:"E (GPa)"},
724
+ {key:"yield_strength_mpa", label:"Yield"},
725
+ {key:"uts_mpa", label:"UTS"},
726
+ {key:"elongation_pct", label:"Elong %"},
727
+ {key:"cost_usd_kg", label:"Cost"},
728
+ {key:"crashworthiness_index", label:"Crash"},
729
+ {key:"sustainability_score", label:"Sustain"},
730
+ {key:"source", label:"Source"},
731
+ {key:"confidence_score", label:"Conf", digits:2}
732
+ ])}
733
+ `;
734
+ plotBarH("e1", familySummary(), "energy", "family", "Energy Absorption Potential", true);
735
+ plotScatter("e2", filteredScatter(), "cost_usd_kg", "sustainability_score", "family", "crashworthiness_index", "Cost vs Sustainability");
736
+ plotCurves("e3", curveIds);
737
+ return;
738
+ }
739
+
740
+ if (state.tab === "scenario") {
741
+ const top = scenarioRecs();
742
+ const best = top[0] || {};
743
+ root.innerHTML = `
744
+ <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Crash Scenario Intelligence</h3>
745
+ <p style="color:var(--muted)">Recommendations for <strong>${state.scenario}</strong> on <strong>${state.component}</strong>.</p>
746
+ <div class="metrics">
747
+ <div class="metric"><div class="label">Top Crash Score</div><div class="value">${num(best.crash_score || 0)}</div></div>
748
+ <div class="metric"><div class="label">Best Weight Reduction</div><div class="value">${num(best.weight_reduction_pct || 0)}%</div></div>
749
+ <div class="metric"><div class="label">Top Material</div><div class="value" style="font-size:1.05rem">${best.material_name || "—"}</div></div>
750
+ <div class="metric"><div class="label">Family</div><div class="value" style="font-size:1.05rem">${best.family || "—"}</div></div>
751
+ <div class="metric"><div class="label">Joining</div><div class="value" style="font-size:0.95rem">${best.joining_method || "—"}</div></div>
752
+ </div>
753
+ <div class="grid-2">
754
+ <div class="card"><div id="s1"></div></div>
755
+ <div class="card"><div id="s2"></div></div>
756
+ </div>
757
+ <div class="card"><div id="s3"></div></div>
758
+ ${tableHTML(top, [
759
+ {key:"material_name", label:"Material"},
760
+ {key:"family", label:"Family"},
761
+ {key:"thickness_mm", label:"Thickness"},
762
+ {key:"joining_method", label:"Joining"},
763
+ {key:"crash_score", label:"Crash"},
764
+ {key:"energy_absorption_kj", label:"Energy"},
765
+ {key:"intrusion_mm", label:"Intrusion"},
766
+ {key:"weight_reduction_pct", label:"ΔWeight %"},
767
+ {key:"simulation_risk", label:"Sim Risk"}
768
+ ])}
769
+ <div class="note"><strong>Recommended action:</strong> Evaluate <em>${best.material_name || "candidate"}</em>
770
+ (${best.family || ""}) at ~${num(best.thickness_mm || 2, 2)} mm with <em>${best.joining_method || "hybrid joining"}</em>.
771
+ Expected crash score ${num(best.crash_score || 0)}. Run component-level ${state.scenario.toLowerCase()} CAE before physical validation.</div>
772
+ `;
773
+ plotBarH("s1", top.map(r => ({ family: r.material_name, crash: r.crash_score })), "crash", "family", "Top Recommended Materials", true);
774
+ plotScatter("s2", DATA.energy_intrusion.filter(r => state.families.includes(r.family)), "intrusion_mm", "energy_absorption_kj", "crash_scenario", null, "Energy Absorption vs Intrusion");
775
+ plotBarH("s3", DATA.scenario_kpi.map(r => ({ family: r.crash_scenario, crash: r.avg_crash_score })), "crash", "family", "Average Crash Score by Scenario", true);
776
+ return;
777
+ }
778
+
779
+ if (state.tab === "compare") {
780
+ const ranked = rankedMaterials(8);
781
+ root.innerHTML = `
782
+ <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Material Comparison & Ranking</h3>
783
+ <div class="grid-2">
784
+ <div class="card"><div id="c1"></div></div>
785
+ <div class="card"><div id="c2"></div></div>
786
+ </div>
787
+ ${tableHTML(ranked, [
788
+ {key:"material_name", label:"Material"},
789
+ {key:"family", label:"Family"},
790
+ {key:"mo_score", label:"MO Score"},
791
+ {key:"crashworthiness_index", label:"Crash"},
792
+ {key:"lightweighting_score", label:"Weight"},
793
+ {key:"cost_performance_score", label:"Cost-Perf"},
794
+ {key:"sustainability_score", label:"Sustain"},
795
+ {key:"failure_risk", label:"Fail Risk", digits:3},
796
+ {key:"uts_mpa", label:"UTS"},
797
+ {key:"density_g_cm3", label:"Density"},
798
+ {key:"cost_usd_kg", label:"Cost"}
799
+ ])}
800
+ `;
801
+ plotRadar("c1", ranked);
802
+ plotBarH("c2", ranked.map(r => ({ family: r.material_name, crash: r.mo_score })), "crash", "family", "Multi-Objective Score (Top Materials)", true);
803
+ return;
804
+ }
805
+
806
+ if (state.tab === "cards") {
807
+ const ranked = rankedMaterials(20);
808
+ const options = ranked.length ? ranked : filt.slice(0, 20);
809
+ root.innerHTML = `
810
+ <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">CAE Material Card Generator</h3>
811
+ <p style="color:var(--muted)">Generate draft solver-ready material cards with elastic modulus, yield, plastic curve, strain-rate sensitivity, failure strain, and confidence score.</p>
812
+ <div class="controls-row">
813
+ <div><label>Select material</label><select id="cardMat">${options.map(r => `<option value="${r.material_name}">${r.material_name} (${r.family})</option>`).join("")}</select></div>
814
+ <div><label>Target solver</label><select id="cardSolver">${DATA.solvers.map(s => `<option>${s}</option>`).join("")}</select></div>
815
+ <div><label>&nbsp;</label><button id="dlCard">Download card</button></div>
816
+ </div>
817
+ <div class="metrics" id="cardMetrics"></div>
818
+ <div class="grid-2">
819
+ <pre class="card-text" id="cardText"></pre>
820
+ <div class="card"><div id="cardPlot"></div><div class="note" id="cardNote"></div></div>
821
+ </div>
822
+ `;
823
+ const updateCard = () => {
824
+ const name = document.getElementById("cardMat").value;
825
+ const solver = document.getElementById("cardSolver").value;
826
+ const mat = options.find(r => r.material_name === name) || options[0];
827
+ if (!mat) return;
828
+ const card = materialCard(mat, solver);
829
+ document.getElementById("cardText").textContent = card.text;
830
+ document.getElementById("cardMetrics").innerHTML = `
831
+ <div class="metric"><div class="label">Card Type</div><div class="value" style="font-size:1rem">${card.cardType.split(" ")[0]}</div></div>
832
+ <div class="metric"><div class="label">Yield (MPa)</div><div class="value">${num(mat.yield_strength_mpa, 0)}</div></div>
833
+ <div class="metric"><div class="label">Failure Strain</div><div class="value">${num(mat.failure_strain, 3)}</div></div>
834
+ <div class="metric"><div class="label">Confidence</div><div class="value">${num(mat.confidence_score, 2)}</div></div>
835
+ <div class="metric"><div class="label">UTS (MPa)</div><div class="value">${num(mat.uts_mpa, 0)}</div></div>`;
836
+ document.getElementById("cardNote").innerHTML = `<strong>Validation status:</strong> Draft — public-data prototype<br/><strong>Damage model:</strong> Linear softening to zero stress at failure strain<br/><strong>Temperature:</strong> Room-temperature card; scale factors TBD`;
837
+ if (DATA.curves[mat.material_id]) {
838
+ plotCurves("cardPlot", [mat.material_id]);
839
+ } else {
840
+ Plotly.newPlot("cardPlot", [{
841
+ type: "scatter", mode: "lines+markers",
842
+ x: card.strains, y: card.stresses,
843
+ line: { color: "#0B6E4F", width: 3 }, name: "Plastic curve"
844
+ }], { ...LAYOUT, title: "Draft Plastic Curve", height: 380, xaxis: { title: "Plastic Strain" }, yaxis: { title: "Stress (MPa)" } }, { responsive: true, displayModeBar: false });
845
+ }
846
+ document.getElementById("dlCard").onclick = () => {
847
+ const blob = new Blob([card.text], { type: "text/plain" });
848
+ const a = document.createElement("a");
849
+ a.href = URL.createObjectURL(blob);
850
+ a.download = `${mat.material_name}_${solver.replaceAll(" ", "_")}.k`;
851
+ a.click();
852
+ };
853
+ };
854
+ document.getElementById("cardMat").addEventListener("change", updateCard);
855
+ document.getElementById("cardSolver").addEventListener("change", updateCard);
856
+ updateCard();
857
+ return;
858
+ }
859
+
860
+ if (state.tab === "validation") {
861
+ const rows = DATA.validation.filter(r => state.families.includes(r.family));
862
+ const meanErr = rows.length ? rows.reduce((s, r) => s + r.ai_cae_error_pct, 0) / rows.length : 0;
863
+ const passRate = rows.length ? rows.filter(r => r.pass_fail === "Pass").length / rows.length : 0;
864
+ const meanStar = rows.length ? rows.reduce((s, r) => s + r.nhtsa_star_proxy, 0) / rows.length : 0;
865
+ root.innerHTML = `
866
+ <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Validation Workflow</h3>
867
+ <p style="color:var(--muted)">Compare AI predictions with CAE and physical crash proxies aligned to Euro NCAP / FMVSS / IIHS.</p>
868
+ <div class="metrics">
869
+ <div class="metric"><div class="label">Validation pairs</div><div class="value">${rows.length.toLocaleString()}</div></div>
870
+ <div class="metric"><div class="label">Mean AI–CAE error</div><div class="value">${num(meanErr)}%</div></div>
871
+ <div class="metric"><div class="label">Pass rate</div><div class="value">${pct(passRate)}</div></div>
872
+ <div class="metric"><div class="label">Mean NHTSA-star proxy</div><div class="value">${num(meanStar)}</div></div>
873
+ <div class="metric"><div class="label">Standards</div><div class="value" style="font-size:0.95rem">NCAP/FMVSS</div></div>
874
+ </div>
875
+ <div class="grid-2">
876
+ <div class="card"><div id="v1"></div></div>
877
+ <div class="card"><div id="v2"></div></div>
878
+ </div>
879
+ <div class="note"><strong>Coupon tests:</strong> Tensile, compression, shear, strain-rate, fracture.</div>
880
+ <div class="note"><strong>Component tests:</strong> Bumper beam, crash box, rail, door beam, battery enclosure.</div>
881
+ <div class="note"><strong>CAE validation:</strong> Compare AI prediction with LS-DYNA / Abaqus / PAM-CRASH.</div>
882
+ <div class="note"><strong>Physical crash:</strong> Compare simulation with crash-test measurements.</div>
883
+ <div class="note"><strong>Certification:</strong> Euro NCAP, FMVSS, IIHS, OEM internal standards.</div>
884
+ ${tableHTML(rows.slice(0, 150), [
885
+ {key:"family", label:"Family"},
886
+ {key:"crash_scenario", label:"Scenario"},
887
+ {key:"ai_crash_score", label:"AI"},
888
+ {key:"cae_crash_score", label:"CAE"},
889
+ {key:"physical_crash_score", label:"Physical"},
890
+ {key:"ai_cae_error_pct", label:"Error %"},
891
+ {key:"pass_fail", label:"Status"},
892
+ {key:"nhtsa_star_proxy", label:"Stars", digits:0},
893
+ {key:"standard", label:"Standard"}
894
+ ])}
895
+ `;
896
+ plotValidation("v1", "v2");
897
+ return;
898
+ }
899
+
900
+ // library
901
+ root.innerHTML = `
902
+ <h3 style="color:var(--green-dark);font-family:IBM Plex Sans,sans-serif">Data Library & Export</h3>
903
+ <p style="color:var(--muted)">Public-style material and crash datasets used by the ranking and card-generation engines.</p>
904
+ <div class="controls-row">
905
+ <div><label>Dataset</label>
906
+ <select id="libSet">
907
+ <option value="table">materials</option>
908
+ <option value="validation">validation</option>
909
+ <option value="top_materials">top_materials</option>
910
+ <option value="scenario_kpi">scenario_kpi</option>
911
+ </select>
912
+ </div>
913
+ <div><label>&nbsp;</label><button id="dlCsv">Download CSV</button></div>
914
+ </div>
915
+ <div id="libTable"></div>
916
+ `;
917
+ const renderLib = () => {
918
+ const key = document.getElementById("libSet").value;
919
+ let rows = DATA[key];
920
+ if (key === "table" || key === "validation") rows = rows.filter(r => !r.family || state.families.includes(r.family));
921
+ const cols = Object.keys(rows[0] || {}).slice(0, 12).map(k => ({ key: k, label: k, digits: 3 }));
922
+ document.getElementById("libTable").innerHTML = tableHTML(rows.slice(0, 250), cols);
923
+ document.getElementById("dlCsv").onclick = () => {
924
+ const header = Object.keys(rows[0] || {});
925
+ const csv = [header.join(",")].concat(rows.map(r => header.map(h => JSON.stringify(r[h] ?? "")).join(","))).join("\n");
926
+ const a = document.createElement("a");
927
+ a.href = URL.createObjectURL(new Blob([csv], { type: "text/csv" }));
928
+ a.download = `${key}.csv`;
929
+ a.click();
930
+ };
931
+ };
932
+ document.getElementById("libSet").addEventListener("change", renderLib);
933
+ renderLib();
934
+ }
935
+
936
+ fetch("./data/dashboard_payload.json")
937
+ .then(r => {
938
+ if (!r.ok) throw new Error("Failed to load dashboard payload");
939
+ return r.json();
940
+ })
941
+ .then(json => {
942
+ DATA = json;
943
+ state.families = json.meta.families.slice();
944
+ state.scenario = json.meta.scenarios.includes("Side Impact") ? "Side Impact" : json.meta.scenarios[0];
945
+ state.component = json.meta.components.includes("Battery Enclosure") ? "Battery Enclosure" : json.meta.components[0];
946
+ renderShell();
947
+ })
948
+ .catch(err => {
949
+ document.getElementById("app").innerHTML = `<div class="loading" style="color:#C44536">Failed to load dashboard: ${err.message}</div>`;
950
+ });
951
+ </script>
952
+ </body>
953
  </html>