shrut27 commited on
Commit
f5823da
·
verified ·
1 Parent(s): 2f9e160

Upload folder using huggingface_hub

Browse files
Dockerfile ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.12-slim
2
+
3
+ WORKDIR /app
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install --no-cache-dir -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ EXPOSE 7860
11
+
12
+ HEALTHCHECK CMD curl --fail http://localhost:7860/_stcore/health || exit 1
13
+
14
+ CMD ["streamlit", "run", "app.py", "--server.port=7860", "--server.address=0.0.0.0", "--server.headless=true"]
README.md CHANGED
@@ -1,10 +1,34 @@
1
  ---
2
- title: Crash Intelligence
3
- emoji: 🚀
4
- colorFrom: gray
5
- colorTo: indigo
6
  sdk: static
7
  pinned: false
 
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Crash Intelligence Platform
3
+ emoji: 🛡️
4
+ colorFrom: green
5
+ colorTo: blue
6
  sdk: static
7
  pinned: false
8
+ short_description: AI crash material ranking and CAE card generation
9
  ---
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.
app.py ADDED
@@ -0,0 +1,580 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Crash Intelligence — AI-powered automotive crash material platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import pandas as pd
8
+ import plotly.graph_objects as go
9
+ import streamlit as st
10
+
11
+ from utils.calculations import (
12
+ available_families,
13
+ card_to_text,
14
+ family_summary,
15
+ generate_material_card,
16
+ rank_materials,
17
+ recommend_for_scenario,
18
+ scenario_kpi,
19
+ )
20
+ from utils.data_generator import (
21
+ COMPONENTS,
22
+ CRASH_SCENARIOS,
23
+ JOINING_METHODS,
24
+ SOLVERS,
25
+ generate_all,
26
+ )
27
+ from utils.visualizations import (
28
+ cost_sustain_bubble,
29
+ energy_intrusion_scatter,
30
+ family_bar,
31
+ kpi_gauge,
32
+ radar_materials,
33
+ scatter_crash_vs_weight,
34
+ scenario_heatmap,
35
+ stress_strain_curves,
36
+ top_recommendations_bar,
37
+ validation_error_hist,
38
+ validation_parity,
39
+ )
40
+
41
+ DATA_DIR = Path(__file__).resolve().parent / "data"
42
+
43
+ st.set_page_config(
44
+ page_title="Crash Intelligence Platform",
45
+ page_icon="🛡️",
46
+ layout="wide",
47
+ initial_sidebar_state="expanded",
48
+ )
49
+
50
+ st.markdown(
51
+ """
52
+ <style>
53
+ @import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;600;700&family=IBM+Plex+Sans:wght@500;600&display=swap');
54
+
55
+ html, body, [class*="css"] {
56
+ font-family: 'Source Sans 3', 'Segoe UI', sans-serif;
57
+ color: #0f172a;
58
+ }
59
+ .block-container { padding-top: 1.2rem; padding-bottom: 2rem; max-width: 1400px; }
60
+ h1, h2, h3 { font-family: 'IBM Plex Sans', sans-serif !important; color: #0B3D2E !important; }
61
+ div[data-testid="stMetricValue"] { font-size: 1.6rem; color: #0B6E4F; }
62
+ div[data-testid="stMetricLabel"] { color: #334155; }
63
+ section[data-testid="stSidebar"] {
64
+ background: linear-gradient(180deg, #0B3D2E 0%, #1B4965 100%);
65
+ }
66
+ section[data-testid="stSidebar"] * { color: #f8fafc !important; }
67
+ section[data-testid="stSidebar"] .stSelectbox label,
68
+ section[data-testid="stSidebar"] .stMultiSelect label,
69
+ section[data-testid="stSidebar"] .stSlider label {
70
+ color: #e2e8f0 !important;
71
+ }
72
+ .hero {
73
+ background: linear-gradient(120deg, #0B3D2E 0%, #1B4965 55%, #5FA8D3 100%);
74
+ color: #ffffff;
75
+ padding: 1.4rem 1.6rem;
76
+ border-radius: 12px;
77
+ margin-bottom: 1rem;
78
+ }
79
+ .hero h1 { color: #ffffff !important; margin: 0 0 0.35rem 0; font-size: 1.9rem; }
80
+ .hero p { color: #e2e8f0; margin: 0; font-size: 1.02rem; }
81
+ .card-box {
82
+ background: #ffffff;
83
+ border: 1px solid #e2e8f0;
84
+ border-left: 4px solid #0B6E4F;
85
+ border-radius: 8px;
86
+ padding: 0.9rem 1rem;
87
+ margin-bottom: 0.6rem;
88
+ color: #0f172a;
89
+ }
90
+ .stTabs [data-baseweb="tab"] { color: #0f172a; font-weight: 600; }
91
+ .stDataFrame { color: #0f172a; }
92
+ </style>
93
+ """,
94
+ unsafe_allow_html=True,
95
+ )
96
+
97
+
98
+ @st.cache_data(show_spinner="Loading crash material datasets…")
99
+ def load_datasets() -> dict[str, pd.DataFrame]:
100
+ materials_path = DATA_DIR / "materials.csv"
101
+ if not materials_path.exists():
102
+ generate_all(DATA_DIR)
103
+ return {
104
+ "materials": pd.read_csv(DATA_DIR / "materials.csv"),
105
+ "stress_strain": pd.read_csv(DATA_DIR / "stress_strain.csv"),
106
+ "recommendations": pd.read_csv(DATA_DIR / "recommendations.csv"),
107
+ "validation": pd.read_csv(DATA_DIR / "validation.csv"),
108
+ }
109
+
110
+
111
+ def render_hero() -> None:
112
+ st.markdown(
113
+ """
114
+ <div class="hero">
115
+ <h1>Crash Intelligence Platform</h1>
116
+ <p>
117
+ AI-powered material recommendation, prediction, and CAE card generation for
118
+ automotive crash-performance applications across metals, composites, polymers, foams, and adhesives.
119
+ </p>
120
+ </div>
121
+ """,
122
+ unsafe_allow_html=True,
123
+ )
124
+
125
+
126
+ def main() -> None:
127
+ data = load_datasets()
128
+ materials = data["materials"]
129
+ stress = data["stress_strain"]
130
+ recommendations = data["recommendations"]
131
+ validation = data["validation"]
132
+
133
+ render_hero()
134
+
135
+ families = available_families()
136
+ with st.sidebar:
137
+ st.markdown("### Filters & Targets")
138
+ selected_families = st.multiselect(
139
+ "Material families",
140
+ options=families,
141
+ default=families[:8],
142
+ )
143
+ scenario = st.selectbox("Crash scenario", CRASH_SCENARIOS, index=0)
144
+ component = st.selectbox("Vehicle component", COMPONENTS, index=8)
145
+ max_cost = st.slider("Max cost (USD/kg)", 1.0, 80.0, 40.0, 1.0)
146
+ min_uts = st.slider("Min UTS (MPa)", 20, 1800, 200, 20)
147
+ max_density = st.slider("Max density (g/cm³)", 0.1, 8.0, 8.0, 0.1)
148
+ st.markdown("---")
149
+ st.markdown("### Multi-objective weights")
150
+ w_crash = st.slider("Crash performance", 0.0, 1.0, 0.30, 0.05)
151
+ w_weight = st.slider("Lightweighting", 0.0, 1.0, 0.20, 0.05)
152
+ w_cost = st.slider("Cost performance", 0.0, 1.0, 0.20, 0.05)
153
+ w_sust = st.slider("Sustainability", 0.0, 1.0, 0.15, 0.05)
154
+ w_fail = st.slider("Low failure risk", 0.0, 1.0, 0.15, 0.05)
155
+ weights = {
156
+ "crash": w_crash,
157
+ "weight": w_weight,
158
+ "cost": w_cost,
159
+ "sustainability": w_sust,
160
+ "failure": w_fail,
161
+ }
162
+ st.markdown("---")
163
+ st.caption(
164
+ f"Database: {len(materials):,} materials · "
165
+ f"{len(recommendations):,} scenario predictions · "
166
+ f"{len(validation):,} validation pairs"
167
+ )
168
+
169
+ filt = materials[materials["family"].isin(selected_families)].copy()
170
+ if filt.empty:
171
+ st.warning("No materials match the selected families. Expand the family filter.")
172
+ return
173
+
174
+ filt = filt[
175
+ (filt["cost_usd_kg"] <= max_cost)
176
+ & (filt["uts_mpa"] >= min_uts)
177
+ & (filt["density_g_cm3"] <= max_density)
178
+ ]
179
+ if filt.empty:
180
+ st.warning("No materials match the current property filters. Relax cost / UTS / density limits.")
181
+ return
182
+
183
+ rec_filt = recommendations[recommendations["family"].isin(selected_families)]
184
+
185
+ ranked = rank_materials(
186
+ filt,
187
+ families=selected_families,
188
+ max_cost=max_cost,
189
+ min_uts=min_uts,
190
+ max_density=max_density,
191
+ weights=weights,
192
+ top_n=8,
193
+ )
194
+
195
+ tabs = st.tabs(
196
+ [
197
+ "Overview",
198
+ "Material Explorer",
199
+ "Crash Scenario AI",
200
+ "Compare & Rank",
201
+ "Material Cards",
202
+ "Validation",
203
+ "Data Library",
204
+ ]
205
+ )
206
+
207
+ # --- Overview ---
208
+ with tabs[0]:
209
+ st.subheader("Platform KPIs")
210
+ c1, c2, c3, c4, c5 = st.columns(5)
211
+ c1.metric("Materials", f"{len(filt):,}")
212
+ c2.metric("Avg Crash Index", f"{filt['crashworthiness_index'].mean():.1f}")
213
+ c3.metric("Avg Energy Potential", f"{filt['energy_absorption_potential'].mean():.2f}")
214
+ c4.metric("Avg Sustainability", f"{filt['sustainability_score'].mean():.1f}")
215
+ c5.metric("AI–CAE Pass Rate", f"{(validation['pass_fail']=='Pass').mean()*100:.0f}%")
216
+
217
+ g1, g2, g3 = st.columns(3)
218
+ with g1:
219
+ st.plotly_chart(
220
+ kpi_gauge(float(filt["crashworthiness_index"].mean()), "Crashworthiness", "#0B6E4F"),
221
+ use_container_width=True,
222
+ )
223
+ with g2:
224
+ st.plotly_chart(
225
+ kpi_gauge(float(filt["lightweighting_score"].mean()), "Lightweighting", "#1B4965"),
226
+ use_container_width=True,
227
+ )
228
+ with g3:
229
+ st.plotly_chart(
230
+ kpi_gauge(float(filt["sustainability_score"].mean()), "Sustainability", "#2A9D8F"),
231
+ use_container_width=True,
232
+ )
233
+
234
+ st.markdown("#### Family performance & trade-offs")
235
+ summary = family_summary(filt)
236
+ r1, r2 = st.columns(2)
237
+ with r1:
238
+ st.plotly_chart(
239
+ family_bar(summary, "crashworthiness_index", "Crashworthiness by Family"),
240
+ use_container_width=True,
241
+ )
242
+ with r2:
243
+ st.plotly_chart(scatter_crash_vs_weight(filt), use_container_width=True)
244
+
245
+ st.plotly_chart(scenario_heatmap(rec_filt), use_container_width=True)
246
+
247
+ st.markdown(
248
+ """
249
+ <div class="card-box">
250
+ <strong>Value proposition:</strong> Shortlist safer, lighter, cheaper, and more sustainable
251
+ crash-critical materials before expensive CAE and physical testing — then export draft
252
+ solver-ready material cards for LS-DYNA, Abaqus, PAM-CRASH, and Radioss.
253
+ </div>
254
+ """,
255
+ unsafe_allow_html=True,
256
+ )
257
+
258
+ # --- Material Explorer ---
259
+ with tabs[1]:
260
+ st.subheader("Material Data Explorer")
261
+ st.markdown(
262
+ "Browse standardized mechanical, cost, and sustainability properties across automotive crash materials."
263
+ )
264
+ m1, m2 = st.columns(2)
265
+ with m1:
266
+ st.plotly_chart(
267
+ family_bar(summary, "energy_absorption_potential", "Energy Absorption Potential"),
268
+ use_container_width=True,
269
+ )
270
+ with m2:
271
+ st.plotly_chart(cost_sustain_bubble(filt), use_container_width=True)
272
+
273
+ curve_options = (
274
+ stress[stress["family"].isin(selected_families)][["material_id", "material_name", "family"]]
275
+ .drop_duplicates()
276
+ .head(80)
277
+ )
278
+ if not curve_options.empty:
279
+ pick = st.multiselect(
280
+ "Select materials for stress–strain curves",
281
+ options=curve_options["material_id"].tolist(),
282
+ default=curve_options["material_id"].tolist()[:3],
283
+ format_func=lambda mid: (
284
+ f"{curve_options.loc[curve_options.material_id==mid, 'material_name'].iloc[0]} "
285
+ f"({curve_options.loc[curve_options.material_id==mid, 'family'].iloc[0]})"
286
+ ),
287
+ )
288
+ if pick:
289
+ st.plotly_chart(stress_strain_curves(stress, pick), use_container_width=True)
290
+
291
+ st.dataframe(
292
+ filt[
293
+ [
294
+ "material_name",
295
+ "family",
296
+ "density_g_cm3",
297
+ "youngs_modulus_gpa",
298
+ "yield_strength_mpa",
299
+ "uts_mpa",
300
+ "elongation_pct",
301
+ "failure_strain",
302
+ "cost_usd_kg",
303
+ "co2_kg_kg",
304
+ "crashworthiness_index",
305
+ "sustainability_score",
306
+ "source",
307
+ "confidence_score",
308
+ ]
309
+ ].sort_values("crashworthiness_index", ascending=False),
310
+ use_container_width=True,
311
+ height=360,
312
+ )
313
+
314
+ # --- Crash Scenario AI ---
315
+ with tabs[2]:
316
+ st.subheader("Crash Scenario Intelligence")
317
+ st.markdown(
318
+ f"Recommendations for **{scenario}** on **{component}** using multi-objective AI ranking."
319
+ )
320
+ top_rec = recommend_for_scenario(
321
+ filt,
322
+ rec_filt,
323
+ scenario=scenario,
324
+ component=component,
325
+ families=selected_families,
326
+ top_n=5,
327
+ )
328
+ if top_rec.empty:
329
+ st.info("No recommendations available for this combination.")
330
+ else:
331
+ k1, k2, k3, k4 = st.columns(4)
332
+ k1.metric("Top Crash Score", f"{top_rec['crash_score'].iloc[0]:.1f}")
333
+ k2.metric(
334
+ "Best Weight Reduction",
335
+ f"{top_rec.get('weight_reduction_pct', pd.Series([0])).iloc[0]:.1f}%",
336
+ )
337
+ k3.metric("Top Material", str(top_rec["material_name"].iloc[0]))
338
+ k4.metric("Family", str(top_rec["family"].iloc[0]))
339
+
340
+ st.plotly_chart(top_recommendations_bar(top_rec), use_container_width=True)
341
+ c_a, c_b = st.columns(2)
342
+ with c_a:
343
+ st.plotly_chart(energy_intrusion_scatter(rec_filt), use_container_width=True)
344
+ with c_b:
345
+ sk = scenario_kpi(rec_filt)
346
+ st.plotly_chart(
347
+ family_bar(
348
+ sk.rename(columns={"crash_scenario": "family", "avg_crash_score": "crashworthiness_index"}),
349
+ "crashworthiness_index",
350
+ "Average Crash Score by Scenario",
351
+ ),
352
+ use_container_width=True,
353
+ )
354
+
355
+ st.markdown("#### Top 5 recommendations")
356
+ display_cols = [
357
+ c
358
+ for c in [
359
+ "material_name",
360
+ "family",
361
+ "thickness_mm",
362
+ "joining_method",
363
+ "crash_score",
364
+ "energy_absorption_kj",
365
+ "intrusion_mm",
366
+ "peak_force_kn",
367
+ "crush_force_efficiency",
368
+ "weight_reduction_pct",
369
+ "cost_score",
370
+ "sustainability_score",
371
+ "simulation_risk",
372
+ ]
373
+ if c in top_rec.columns
374
+ ]
375
+ st.dataframe(top_rec[display_cols], use_container_width=True)
376
+
377
+ st.markdown("#### Suggested next steps")
378
+ best = top_rec.iloc[0]
379
+ join = best.get("joining_method", JOINING_METHODS[0])
380
+ thick = best.get("thickness_mm", 2.0)
381
+ st.markdown(
382
+ f"""
383
+ <div class="card-box">
384
+ <strong>Recommended action:</strong> Evaluate <em>{best['material_name']}</em>
385
+ ({best['family']}) at ~{thick} mm with <em>{join}</em> joining.
386
+ Expected crash score {best['crash_score']:.1f}.
387
+ Run component-level {scenario.lower()} CAE before physical validation.
388
+ </div>
389
+ """,
390
+ unsafe_allow_html=True,
391
+ )
392
+
393
+ # --- Compare & Rank ---
394
+ with tabs[3]:
395
+ st.subheader("Material Comparison & Ranking")
396
+ if ranked.empty:
397
+ st.info("No ranked materials under current constraints.")
398
+ else:
399
+ st.plotly_chart(radar_materials(ranked), use_container_width=True)
400
+ left, right = st.columns(2)
401
+ with left:
402
+ st.plotly_chart(
403
+ family_bar(
404
+ ranked.rename(columns={"material_name": "family", "mo_score": "crashworthiness_index"})[
405
+ ["family", "crashworthiness_index"]
406
+ ],
407
+ "crashworthiness_index",
408
+ "Multi-Objective Score (Top Materials)",
409
+ ),
410
+ use_container_width=True,
411
+ )
412
+ with right:
413
+ st.dataframe(
414
+ ranked[
415
+ [
416
+ "material_name",
417
+ "family",
418
+ "mo_score",
419
+ "crashworthiness_index",
420
+ "lightweighting_score",
421
+ "cost_performance_score",
422
+ "sustainability_score",
423
+ "failure_risk",
424
+ "uts_mpa",
425
+ "density_g_cm3",
426
+ "cost_usd_kg",
427
+ ]
428
+ ],
429
+ use_container_width=True,
430
+ height=420,
431
+ )
432
+
433
+ # --- Material Cards ---
434
+ with tabs[4]:
435
+ st.subheader("CAE Material Card Generator")
436
+ st.markdown(
437
+ "Generate draft solver-ready material cards including elastic modulus, yield, "
438
+ "plastic curve, strain-rate sensitivity, failure strain, and confidence score."
439
+ )
440
+ card_mat_name = st.selectbox(
441
+ "Select material",
442
+ options=ranked["material_name"].tolist() if not ranked.empty else filt["material_name"].head(50).tolist(),
443
+ )
444
+ solver = st.selectbox("Target solver", SOLVERS)
445
+ mat_row = filt[filt["material_name"] == card_mat_name]
446
+ if mat_row.empty and not ranked.empty:
447
+ mat_row = ranked[ranked["material_name"] == card_mat_name]
448
+ if mat_row.empty:
449
+ mat_row = materials[materials["material_name"] == card_mat_name]
450
+
451
+ if not mat_row.empty:
452
+ material = mat_row.iloc[0]
453
+ card = generate_material_card(material, solver=solver)
454
+ text = card_to_text(card)
455
+
456
+ mc1, mc2, mc3, mc4 = st.columns(4)
457
+ mc1.metric("Card Type", card["card_type"].split()[0])
458
+ mc2.metric("Yield (MPa)", f"{card['yield_strength_mpa']:.0f}")
459
+ mc3.metric("Failure Strain", f"{card['failure_strain']:.3f}")
460
+ mc4.metric("Confidence", f"{card['confidence_score']:.2f}")
461
+
462
+ col_l, col_r = st.columns([1.1, 0.9])
463
+ with col_l:
464
+ st.code(text, language="text")
465
+ st.download_button(
466
+ "Download material card",
467
+ data=text,
468
+ file_name=f"{material['material_name']}_{solver.replace(' ', '_')}.k",
469
+ mime="text/plain",
470
+ )
471
+ with col_r:
472
+ curve_id = material["material_id"] if "material_id" in material.index else None
473
+ if curve_id and curve_id in stress["material_id"].values:
474
+ st.plotly_chart(
475
+ stress_strain_curves(stress, [curve_id]),
476
+ use_container_width=True,
477
+ )
478
+ else:
479
+ fig = go.Figure()
480
+ fig.add_trace(
481
+ go.Scatter(
482
+ x=card["plastic_curve_strain"],
483
+ y=card["plastic_curve_stress_mpa"],
484
+ mode="lines+markers",
485
+ line=dict(color="#0B6E4F", width=3),
486
+ name="Plastic curve",
487
+ )
488
+ )
489
+ fig.update_layout(
490
+ title="Draft Plastic Curve",
491
+ xaxis_title="Plastic Strain",
492
+ yaxis_title="Stress (MPa)",
493
+ height=400,
494
+ paper_bgcolor="white",
495
+ plot_bgcolor="#f8fafc",
496
+ font=dict(color="#1a1a1a"),
497
+ )
498
+ st.plotly_chart(fig, use_container_width=True)
499
+
500
+ st.markdown(
501
+ f"""
502
+ <div class="card-box">
503
+ <strong>Validation status:</strong> {card['validation_status']}<br/>
504
+ <strong>Damage model:</strong> {card['damage_evolution']}<br/>
505
+ <strong>Temperature:</strong> {card['temperature_dependency']}
506
+ </div>
507
+ """,
508
+ unsafe_allow_html=True,
509
+ )
510
+
511
+ # --- Validation ---
512
+ with tabs[5]:
513
+ st.subheader("Validation Workflow")
514
+ st.markdown(
515
+ "Compare AI predictions with CAE and physical crash proxies aligned to Euro NCAP / FMVSS / IIHS."
516
+ )
517
+ val = validation[validation["family"].isin(selected_families)]
518
+ v1, v2, v3, v4 = st.columns(4)
519
+ v1.metric("Validation pairs", f"{len(val):,}")
520
+ v2.metric("Mean AI–CAE error", f"{val['ai_cae_error_pct'].mean():.1f}%")
521
+ v3.metric("Pass rate", f"{(val['pass_fail']=='Pass').mean()*100:.0f}%")
522
+ v4.metric("Mean NHTSA-star proxy", f"{val['nhtsa_star_proxy'].mean():.1f}")
523
+
524
+ vc1, vc2 = st.columns(2)
525
+ with vc1:
526
+ st.plotly_chart(validation_parity(val), use_container_width=True)
527
+ with vc2:
528
+ st.plotly_chart(validation_error_hist(val), use_container_width=True)
529
+
530
+ st.markdown("#### Validation ladder")
531
+ levels = [
532
+ ("Coupon tests", "Tensile, compression, shear, strain-rate, fracture"),
533
+ ("Component tests", "Bumper beam, crash box, rail, door beam, battery enclosure"),
534
+ ("CAE validation", "Compare AI prediction with LS-DYNA / Abaqus / PAM-CRASH"),
535
+ ("Physical crash", "Compare simulation with crash-test measurements"),
536
+ ("Certification", "Euro NCAP, FMVSS, IIHS, OEM internal standards"),
537
+ ]
538
+ for title, desc in levels:
539
+ st.markdown(
540
+ f'<div class="card-box"><strong>{title}:</strong> {desc}</div>',
541
+ unsafe_allow_html=True,
542
+ )
543
+
544
+ st.dataframe(
545
+ val.sort_values("ai_cae_error_pct").head(200),
546
+ use_container_width=True,
547
+ height=320,
548
+ )
549
+
550
+ # --- Data Library ---
551
+ with tabs[6]:
552
+ st.subheader("Data Library & Export")
553
+ st.markdown(
554
+ "Public-style material and crash datasets used by the ranking and card-generation engines."
555
+ )
556
+ dataset_choice = st.selectbox(
557
+ "Dataset",
558
+ ["materials", "recommendations", "validation", "stress_strain"],
559
+ )
560
+ export_df = data[dataset_choice]
561
+ if dataset_choice != "stress_strain":
562
+ if "family" in export_df.columns:
563
+ export_df = export_df[export_df["family"].isin(selected_families)]
564
+ st.dataframe(export_df.head(500), use_container_width=True, height=400)
565
+ st.download_button(
566
+ f"Download {dataset_choice}.csv",
567
+ data=export_df.to_csv(index=False),
568
+ file_name=f"{dataset_choice}.csv",
569
+ mime="text/csv",
570
+ )
571
+
572
+ st.markdown("---")
573
+ st.caption(
574
+ "Crash Intelligence Platform · Prototype powered by public-style material & crash databases · "
575
+ "For OEM production use, calibrate with supplier cards, high strain-rate tests, and full-vehicle CAE."
576
+ )
577
+
578
+
579
+ if __name__ == "__main__":
580
+ main()
data/materials.csv ADDED
The diff for this file is too large to render. See raw diff
 
data/recommendations.csv ADDED
The diff for this file is too large to render. See raw diff
 
data/stress_strain.csv ADDED
The diff for this file is too large to render. See raw diff
 
data/validation.csv ADDED
The diff for this file is too large to render. See raw diff
 
index.html CHANGED
@@ -1 +1,67 @@
1
- <html><body><h1>Crash Intelligence</h1></body></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
+ <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>
requirements.txt ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ streamlit==1.45.0
2
+ pandas==2.2.3
3
+ numpy==1.26.4
4
+ plotly==5.24.1
utils/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ """Crash Intelligence platform utilities."""
utils/calculations.py ADDED
@@ -0,0 +1,215 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Domain calculations for crash material intelligence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+
8
+ from utils.data_generator import MATERIAL_CARD_MAP, MATERIAL_FAMILIES
9
+
10
+
11
+ WEIGHTS_DEFAULT = {
12
+ "crash": 0.30,
13
+ "weight": 0.20,
14
+ "cost": 0.20,
15
+ "sustainability": 0.15,
16
+ "failure": 0.15,
17
+ }
18
+
19
+
20
+ def multi_objective_score(
21
+ df: pd.DataFrame,
22
+ weights: dict[str, float] | None = None,
23
+ ) -> pd.Series:
24
+ """Compute weighted multi-objective ranking score."""
25
+ w = weights or WEIGHTS_DEFAULT
26
+ total = sum(w.values()) or 1.0
27
+ w = {k: v / total for k, v in w.items()}
28
+ score = (
29
+ w["crash"] * df["crashworthiness_index"]
30
+ + w["weight"] * df["lightweighting_score"]
31
+ + w["cost"] * np.clip(df["cost_performance_score"] * 1.2, 0, 100)
32
+ + w["sustainability"] * df["sustainability_score"]
33
+ + w["failure"] * (100 * (1.0 - df["failure_risk"]))
34
+ )
35
+ return score.round(2)
36
+
37
+
38
+ def rank_materials(
39
+ materials: pd.DataFrame,
40
+ families: list[str] | None = None,
41
+ max_cost: float | None = None,
42
+ min_uts: float | None = None,
43
+ max_density: float | None = None,
44
+ weights: dict[str, float] | None = None,
45
+ top_n: int = 5,
46
+ ) -> pd.DataFrame:
47
+ """Filter and rank materials for crash applications."""
48
+ df = materials.copy()
49
+ if families:
50
+ df = df[df["family"].isin(families)]
51
+ if max_cost is not None:
52
+ df = df[df["cost_usd_kg"] <= max_cost]
53
+ if min_uts is not None:
54
+ df = df[df["uts_mpa"] >= min_uts]
55
+ if max_density is not None:
56
+ df = df[df["density_g_cm3"] <= max_density]
57
+ if df.empty:
58
+ return df
59
+ df = df.copy()
60
+ df["mo_score"] = multi_objective_score(df, weights)
61
+ return df.sort_values("mo_score", ascending=False).head(top_n)
62
+
63
+
64
+ def recommend_for_scenario(
65
+ materials: pd.DataFrame,
66
+ recommendations: pd.DataFrame,
67
+ scenario: str,
68
+ component: str,
69
+ families: list[str] | None = None,
70
+ top_n: int = 5,
71
+ ) -> pd.DataFrame:
72
+ """Recommend materials for a crash scenario + component pair."""
73
+ rec = recommendations[
74
+ (recommendations["crash_scenario"] == scenario)
75
+ & (recommendations["component"] == component)
76
+ ].copy()
77
+ if families:
78
+ rec = rec[rec["family"].isin(families)]
79
+ if rec.empty:
80
+ ranked = rank_materials(materials, families=families, top_n=top_n)
81
+ ranked = ranked.copy()
82
+ ranked["crash_scenario"] = scenario
83
+ ranked["component"] = component
84
+ ranked["crash_score"] = ranked["crashworthiness_index"]
85
+ ranked["thickness_mm"] = 2.0
86
+ ranked["joining_method"] = "Hybrid Weld-Bond"
87
+ ranked["simulation_risk"] = (ranked["failure_risk"] * 100).round(2)
88
+ return ranked
89
+
90
+ agg_cols = [
91
+ "crash_score",
92
+ "energy_absorption_kj",
93
+ "intrusion_mm",
94
+ "peak_force_kn",
95
+ "crush_force_efficiency",
96
+ "specific_energy_absorption",
97
+ "weight_reduction_pct",
98
+ "cost_score",
99
+ "sustainability_score",
100
+ "lightweighting_score",
101
+ "simulation_risk",
102
+ "thickness_mm",
103
+ ]
104
+ grouped = (
105
+ rec.groupby(["material_id", "material_name", "family", "joining_method"], as_index=False)[
106
+ agg_cols
107
+ ]
108
+ .mean(numeric_only=True)
109
+ .sort_values("crash_score", ascending=False)
110
+ .head(top_n)
111
+ )
112
+ return grouped
113
+
114
+
115
+ def generate_material_card(material: pd.Series, solver: str = "LS-DYNA") -> dict:
116
+ """Build a draft CAE-ready material card payload."""
117
+ family = material["family"]
118
+ card_type = MATERIAL_CARD_MAP.get(family, "MAT_024")
119
+ curve_pts = 10
120
+ strains = np.linspace(0.0, float(material["failure_strain"]), curve_pts)
121
+ ys = float(material["yield_strength_mpa"])
122
+ uts = float(material["uts_mpa"])
123
+ stresses = []
124
+ for eps in strains:
125
+ if eps <= 0:
126
+ stresses.append(ys)
127
+ else:
128
+ t = min(eps / max(material["failure_strain"], 1e-6), 1.0)
129
+ stresses.append(ys + (uts - ys) * t)
130
+
131
+ card = {
132
+ "solver": solver,
133
+ "card_type": card_type,
134
+ "material_name": material["material_name"],
135
+ "family": family,
136
+ "density_g_cm3": float(material["density_g_cm3"]),
137
+ "youngs_modulus_gpa": float(material["youngs_modulus_gpa"]),
138
+ "poisson_ratio": 0.30 if "Aluminum" in family or family == "Magnesium" else 0.29,
139
+ "yield_strength_mpa": ys,
140
+ "uts_mpa": uts,
141
+ "failure_strain": float(material["failure_strain"]),
142
+ "strain_rate_sensitivity": float(material["strain_rate_sensitivity"]),
143
+ "plastic_curve_strain": [round(float(s), 5) for s in strains],
144
+ "plastic_curve_stress_mpa": [round(float(s), 2) for s in stresses],
145
+ "damage_evolution": "Linear softening to zero stress at failure strain",
146
+ "temperature_dependency": "Room-temperature card; scale factors TBD",
147
+ "validation_status": "Draft — public-data prototype",
148
+ "confidence_score": float(material["confidence_score"]),
149
+ }
150
+ return card
151
+
152
+
153
+ def card_to_text(card: dict) -> str:
154
+ """Serialize a material card to a readable text block."""
155
+ lines = [
156
+ f"*KEYWORD ({card['solver']} draft)",
157
+ f"$ Material: {card['material_name']} ({card['family']})",
158
+ f"$ Card type: {card['card_type']}",
159
+ f"$ Confidence: {card['confidence_score']:.2f}",
160
+ f"$ Validation: {card['validation_status']}",
161
+ "*MAT_PIECEWISE_LINEAR_PLASTICITY",
162
+ f"$ RO (g/cm3) = {card['density_g_cm3']}",
163
+ f"$ E (GPa) = {card['youngs_modulus_gpa']}",
164
+ f"$ PR = {card['poisson_ratio']}",
165
+ f"$ SIGY (MPa) = {card['yield_strength_mpa']}",
166
+ f"$ FAIL = {card['failure_strain']}",
167
+ f"$ C (strain-rate) = {card['strain_rate_sensitivity']}",
168
+ "$ Plastic curve (strain, stress MPa):",
169
+ ]
170
+ for eps, sig in zip(card["plastic_curve_strain"], card["plastic_curve_stress_mpa"]):
171
+ lines.append(f"$ {eps:.5f}, {sig:.2f}")
172
+ lines.append(f"$ Damage: {card['damage_evolution']}")
173
+ lines.append(f"$ Temperature: {card['temperature_dependency']}")
174
+ lines.append("*END")
175
+ return "\n".join(lines)
176
+
177
+
178
+ def family_summary(materials: pd.DataFrame) -> pd.DataFrame:
179
+ """Aggregate key metrics by material family."""
180
+ cols = [
181
+ "density_g_cm3",
182
+ "uts_mpa",
183
+ "crashworthiness_index",
184
+ "energy_absorption_potential",
185
+ "cost_usd_kg",
186
+ "sustainability_score",
187
+ "lightweighting_score",
188
+ "failure_risk",
189
+ ]
190
+ return (
191
+ materials.groupby("family")[cols]
192
+ .mean(numeric_only=True)
193
+ .reset_index()
194
+ .sort_values("crashworthiness_index", ascending=False)
195
+ )
196
+
197
+
198
+ def scenario_kpi(recommendations: pd.DataFrame) -> pd.DataFrame:
199
+ """KPI rollup by crash scenario."""
200
+ return (
201
+ recommendations.groupby("crash_scenario")
202
+ .agg(
203
+ avg_crash_score=("crash_score", "mean"),
204
+ avg_energy=("energy_absorption_kj", "mean"),
205
+ avg_intrusion=("intrusion_mm", "mean"),
206
+ avg_weight_reduction=("weight_reduction_pct", "mean"),
207
+ n_cases=("rec_id", "count"),
208
+ )
209
+ .reset_index()
210
+ .sort_values("avg_crash_score", ascending=False)
211
+ )
212
+
213
+
214
+ def available_families() -> list[str]:
215
+ return list(MATERIAL_FAMILIES.keys())
utils/data_generator.py ADDED
@@ -0,0 +1,535 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Synthetic public-style material and crash datasets for the Crash Intelligence platform."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ import numpy as np
8
+ import pandas as pd
9
+
10
+ RNG = np.random.default_rng(42)
11
+
12
+ MATERIAL_FAMILIES = {
13
+ "Mild Steel": {
14
+ "grades": ["AISI 1008", "AISI 1010", "DC04", "DC06", "St14"],
15
+ "density": (7.80, 7.87),
16
+ "modulus": (200, 210),
17
+ "yield": (140, 280),
18
+ "uts": (270, 420),
19
+ "elongation": (25, 45),
20
+ "failure_strain": (0.25, 0.55),
21
+ "cost": (0.6, 1.2),
22
+ "co2": (1.8, 2.5),
23
+ "recyclability": (0.85, 0.98),
24
+ "strain_rate_sens": (0.01, 0.04),
25
+ },
26
+ "AHSS": {
27
+ "grades": ["DP600", "DP800", "DP1000", "TRIP780", "CP800", "MS1200"],
28
+ "density": (7.80, 7.85),
29
+ "modulus": (200, 210),
30
+ "yield": (350, 900),
31
+ "uts": (600, 1200),
32
+ "elongation": (8, 25),
33
+ "failure_strain": (0.08, 0.22),
34
+ "cost": (1.0, 2.2),
35
+ "co2": (2.0, 2.8),
36
+ "recyclability": (0.80, 0.95),
37
+ "strain_rate_sens": (0.02, 0.06),
38
+ },
39
+ "UHSS": {
40
+ "grades": ["MS1500", "MS1700", "PHS1500", "PHS1800", "QP980", "QP1180"],
41
+ "density": (7.80, 7.85),
42
+ "modulus": (200, 210),
43
+ "yield": (900, 1500),
44
+ "uts": (1200, 2000),
45
+ "elongation": (4, 12),
46
+ "failure_strain": (0.04, 0.12),
47
+ "cost": (1.8, 3.5),
48
+ "co2": (2.2, 3.2),
49
+ "recyclability": (0.75, 0.92),
50
+ "strain_rate_sens": (0.015, 0.05),
51
+ },
52
+ "Boron Steel": {
53
+ "grades": ["22MnB5", "30MnB5", "Usibor 1500", "Usibor 2000", "Ductibor 500"],
54
+ "density": (7.80, 7.85),
55
+ "modulus": (200, 210),
56
+ "yield": (1000, 1400),
57
+ "uts": (1400, 2000),
58
+ "elongation": (5, 10),
59
+ "failure_strain": (0.05, 0.10),
60
+ "cost": (2.0, 3.8),
61
+ "co2": (2.3, 3.4),
62
+ "recyclability": (0.78, 0.93),
63
+ "strain_rate_sens": (0.02, 0.045),
64
+ },
65
+ "Aluminum 5xxx": {
66
+ "grades": ["AA5052", "AA5083", "AA5182", "AA5754", "AA5454"],
67
+ "density": (2.66, 2.70),
68
+ "modulus": (68, 72),
69
+ "yield": (90, 220),
70
+ "uts": (190, 320),
71
+ "elongation": (12, 30),
72
+ "failure_strain": (0.15, 0.35),
73
+ "cost": (2.5, 4.0),
74
+ "co2": (8.0, 12.0),
75
+ "recyclability": (0.90, 0.98),
76
+ "strain_rate_sens": (0.01, 0.03),
77
+ },
78
+ "Aluminum 6xxx": {
79
+ "grades": ["AA6005", "AA6061", "AA6063", "AA6082", "AA6111"],
80
+ "density": (2.68, 2.71),
81
+ "modulus": (68, 72),
82
+ "yield": (150, 280),
83
+ "uts": (220, 340),
84
+ "elongation": (8, 18),
85
+ "failure_strain": (0.10, 0.22),
86
+ "cost": (2.8, 4.5),
87
+ "co2": (8.5, 13.0),
88
+ "recyclability": (0.90, 0.98),
89
+ "strain_rate_sens": (0.01, 0.035),
90
+ },
91
+ "Aluminum 7xxx": {
92
+ "grades": ["AA7003", "AA7020", "AA7075", "AA7050", "AA7085"],
93
+ "density": (2.78, 2.82),
94
+ "modulus": (70, 73),
95
+ "yield": (300, 500),
96
+ "uts": (400, 580),
97
+ "elongation": (5, 12),
98
+ "failure_strain": (0.06, 0.14),
99
+ "cost": (4.0, 7.0),
100
+ "co2": (10.0, 16.0),
101
+ "recyclability": (0.85, 0.95),
102
+ "strain_rate_sens": (0.008, 0.025),
103
+ },
104
+ "Magnesium": {
105
+ "grades": ["AZ31B", "AZ61", "AZ91", "AM60", "ZK60"],
106
+ "density": (1.74, 1.82),
107
+ "modulus": (42, 48),
108
+ "yield": (120, 220),
109
+ "uts": (200, 320),
110
+ "elongation": (8, 18),
111
+ "failure_strain": (0.08, 0.20),
112
+ "cost": (4.5, 8.0),
113
+ "co2": (15.0, 25.0),
114
+ "recyclability": (0.70, 0.90),
115
+ "strain_rate_sens": (0.02, 0.05),
116
+ },
117
+ "CFRP": {
118
+ "grades": ["T700/Epoxy", "T800/Epoxy", "IM7/PEEK", "M55J/Epoxy", "AS4/Epoxy"],
119
+ "density": (1.50, 1.65),
120
+ "modulus": (70, 150),
121
+ "yield": (600, 1200),
122
+ "uts": (800, 1800),
123
+ "elongation": (1.2, 2.5),
124
+ "failure_strain": (0.012, 0.025),
125
+ "cost": (25, 80),
126
+ "co2": (20, 45),
127
+ "recyclability": (0.20, 0.45),
128
+ "strain_rate_sens": (0.005, 0.02),
129
+ },
130
+ "GFRP": {
131
+ "grades": ["E-Glass/Epoxy", "S-Glass/Epoxy", "E-Glass/PP", "E-Glass/PA6", "SMC"],
132
+ "density": (1.80, 2.10),
133
+ "modulus": (20, 45),
134
+ "yield": (200, 450),
135
+ "uts": (300, 700),
136
+ "elongation": (1.5, 4.0),
137
+ "failure_strain": (0.015, 0.04),
138
+ "cost": (5, 18),
139
+ "co2": (4, 12),
140
+ "recyclability": (0.25, 0.50),
141
+ "strain_rate_sens": (0.01, 0.03),
142
+ },
143
+ "Natural Fiber Composite": {
144
+ "grades": ["Flax/PP", "Hemp/PLA", "Jute/Epoxy", "Kenaf/PP", "Bamboo/Epoxy"],
145
+ "density": (1.20, 1.50),
146
+ "modulus": (8, 25),
147
+ "yield": (60, 150),
148
+ "uts": (80, 200),
149
+ "elongation": (2, 6),
150
+ "failure_strain": (0.02, 0.06),
151
+ "cost": (3, 10),
152
+ "co2": (0.5, 3.0),
153
+ "recyclability": (0.50, 0.80),
154
+ "strain_rate_sens": (0.015, 0.04),
155
+ },
156
+ "Polymer": {
157
+ "grades": ["PP-GF30", "PA6-GF35", "ABS", "PC/ABS", "PBT-GF30", "TPU"],
158
+ "density": (0.95, 1.45),
159
+ "modulus": (1.5, 12),
160
+ "yield": (25, 120),
161
+ "uts": (30, 160),
162
+ "elongation": (5, 80),
163
+ "failure_strain": (0.05, 1.5),
164
+ "cost": (1.5, 6.0),
165
+ "co2": (2.0, 6.5),
166
+ "recyclability": (0.40, 0.85),
167
+ "strain_rate_sens": (0.03, 0.10),
168
+ },
169
+ "Elastomer": {
170
+ "grades": ["EPDM", "NR", "SBR", "NBR", "Silicone"],
171
+ "density": (0.90, 1.25),
172
+ "modulus": (0.005, 0.05),
173
+ "yield": (2, 15),
174
+ "uts": (5, 30),
175
+ "elongation": (200, 600),
176
+ "failure_strain": (2.0, 6.0),
177
+ "cost": (2.0, 8.0),
178
+ "co2": (2.5, 7.0),
179
+ "recyclability": (0.15, 0.40),
180
+ "strain_rate_sens": (0.05, 0.15),
181
+ },
182
+ "Structural Foam": {
183
+ "grades": ["EPS", "EPP", "PUR Foam", "Al Honeycomb", "PET Foam"],
184
+ "density": (0.03, 0.25),
185
+ "modulus": (0.01, 2.0),
186
+ "yield": (0.2, 8),
187
+ "uts": (0.3, 12),
188
+ "elongation": (5, 80),
189
+ "failure_strain": (0.4, 2.0),
190
+ "cost": (1.0, 15.0),
191
+ "co2": (1.5, 8.0),
192
+ "recyclability": (0.20, 0.70),
193
+ "strain_rate_sens": (0.04, 0.12),
194
+ },
195
+ "Adhesive": {
196
+ "grades": ["Epoxy Structural", "PU Crash", "Acrylic", "MS Polymer", "Toughened Epoxy"],
197
+ "density": (1.05, 1.40),
198
+ "modulus": (0.5, 4.0),
199
+ "yield": (10, 45),
200
+ "uts": (15, 60),
201
+ "elongation": (5, 100),
202
+ "failure_strain": (0.05, 1.2),
203
+ "cost": (8, 35),
204
+ "co2": (3.0, 10.0),
205
+ "recyclability": (0.05, 0.25),
206
+ "strain_rate_sens": (0.02, 0.08),
207
+ },
208
+ }
209
+
210
+ COMPONENTS = [
211
+ "Bumper Beam",
212
+ "Crash Box",
213
+ "Front Rail",
214
+ "Door Intrusion Beam",
215
+ "B-Pillar",
216
+ "A-Pillar",
217
+ "Roof Rail",
218
+ "Seat Structure",
219
+ "Battery Enclosure",
220
+ "Underbody Shield",
221
+ "Hood Inner",
222
+ "Crossmember",
223
+ ]
224
+
225
+ CRASH_SCENARIOS = [
226
+ "Frontal Crash",
227
+ "Side Impact",
228
+ "Rear Impact",
229
+ "Pole Impact",
230
+ "Pedestrian Impact",
231
+ "Battery Pack Crash",
232
+ "Bumper Beam Crash",
233
+ "Door Intrusion",
234
+ "Seat Structure Crash",
235
+ "BIW Crash",
236
+ "EV Underbody Protection",
237
+ ]
238
+
239
+ JOINING_METHODS = [
240
+ "Spot Weld",
241
+ "Laser Weld",
242
+ "MIG Weld",
243
+ "SPR (Self-Piercing Rivet)",
244
+ "Structural Adhesive",
245
+ "Hybrid Weld-Bond",
246
+ "Bolt Fastened",
247
+ "FDS (Flow Drill Screw)",
248
+ ]
249
+
250
+ SOLVERS = ["LS-DYNA", "Abaqus Explicit", "PAM-CRASH", "Radioss"]
251
+
252
+ MATERIAL_CARD_MAP = {
253
+ "Mild Steel": "MAT_024",
254
+ "AHSS": "MAT_024",
255
+ "UHSS": "MAT_024",
256
+ "Boron Steel": "MAT_024",
257
+ "Aluminum 5xxx": "MAT_024",
258
+ "Aluminum 6xxx": "MAT_024",
259
+ "Aluminum 7xxx": "MAT_024",
260
+ "Magnesium": "MAT_024",
261
+ "CFRP": "MAT_054 / Composite Damage",
262
+ "GFRP": "MAT_054 / Composite Damage",
263
+ "Natural Fiber Composite": "MAT_054 / Composite Damage",
264
+ "Polymer": "MAT_187 / SAMP-1",
265
+ "Elastomer": "MAT_077 / Hyperelastic",
266
+ "Structural Foam": "MAT_063 / Crushable Foam",
267
+ "Adhesive": "MAT_240 / Cohesive Zone",
268
+ }
269
+
270
+
271
+ def _sample_range(bounds: tuple[float, float], n: int) -> np.ndarray:
272
+ lo, hi = bounds
273
+ return RNG.uniform(lo, hi, n)
274
+
275
+
276
+ def generate_materials(n_records: int = 6000) -> pd.DataFrame:
277
+ """Generate a large public-style automotive crash materials database."""
278
+ families = list(MATERIAL_FAMILIES.keys())
279
+ rows: list[dict] = []
280
+
281
+ per_family = max(1, n_records // len(families))
282
+ for family in families:
283
+ spec = MATERIAL_FAMILIES[family]
284
+ n = per_family
285
+ grades = spec["grades"]
286
+ for i in range(n):
287
+ grade = grades[i % len(grades)]
288
+ density = float(_sample_range(spec["density"], 1)[0])
289
+ modulus = float(_sample_range(spec["modulus"], 1)[0])
290
+ yield_s = float(_sample_range(spec["yield"], 1)[0])
291
+ uts = float(_sample_range(spec["uts"], 1)[0])
292
+ if uts < yield_s:
293
+ uts = yield_s * RNG.uniform(1.05, 1.35)
294
+ elong = float(_sample_range(spec["elongation"], 1)[0])
295
+ fail = float(_sample_range(spec["failure_strain"], 1)[0])
296
+ cost = float(_sample_range(spec["cost"], 1)[0])
297
+ co2 = float(_sample_range(spec["co2"], 1)[0])
298
+ recycl = float(_sample_range(spec["recyclability"], 1)[0])
299
+ srs = float(_sample_range(spec["strain_rate_sens"], 1)[0])
300
+
301
+ specific_strength = uts / density
302
+ specific_stiffness = modulus / density
303
+ energy_abs_potential = 0.5 * (yield_s + uts) * fail / density
304
+ ductility_index = elong / max(uts / 100.0, 1e-6)
305
+ crash_index = (
306
+ 0.35 * (energy_abs_potential / 50.0)
307
+ + 0.25 * (specific_strength / 200.0)
308
+ + 0.20 * min(fail * 5.0, 1.0)
309
+ + 0.20 * (1.0 / (1.0 + cost / 10.0))
310
+ )
311
+ crash_index = float(np.clip(crash_index * 100, 5, 98))
312
+ strength_weight = specific_strength
313
+ failure_risk = float(np.clip(1.0 - fail * 2.5 + srs * 2.0, 0.05, 0.95))
314
+ cost_perf = float(np.clip(crash_index / (cost + 0.5), 1, 80))
315
+ sustain = float(
316
+ np.clip(
317
+ 100 * recycl * (1.0 / (1.0 + co2 / 10.0)) * (1.0 / (1.0 + density / 5.0)),
318
+ 5,
319
+ 98,
320
+ )
321
+ )
322
+ lightweight = float(np.clip(100 * (1.0 - density / 8.0) * (specific_strength / 300.0), 5, 98))
323
+
324
+ rows.append(
325
+ {
326
+ "material_id": f"{family[:3].upper()}-{i:04d}",
327
+ "material_name": f"{grade}-{i % 100:02d}",
328
+ "family": family,
329
+ "grade": grade,
330
+ "density_g_cm3": round(density, 3),
331
+ "youngs_modulus_gpa": round(modulus, 2),
332
+ "yield_strength_mpa": round(yield_s, 1),
333
+ "uts_mpa": round(uts, 1),
334
+ "elongation_pct": round(elong, 2),
335
+ "failure_strain": round(fail, 4),
336
+ "strain_rate_sensitivity": round(srs, 4),
337
+ "cost_usd_kg": round(cost, 2),
338
+ "co2_kg_kg": round(co2, 2),
339
+ "recyclability": round(recycl, 3),
340
+ "specific_strength": round(specific_strength, 2),
341
+ "specific_stiffness": round(specific_stiffness, 2),
342
+ "energy_absorption_potential": round(energy_abs_potential, 3),
343
+ "ductility_index": round(ductility_index, 3),
344
+ "crashworthiness_index": round(crash_index, 2),
345
+ "strength_to_weight": round(strength_weight, 2),
346
+ "failure_risk": round(failure_risk, 3),
347
+ "cost_performance_score": round(cost_perf, 2),
348
+ "sustainability_score": round(sustain, 2),
349
+ "lightweighting_score": round(lightweight, 2),
350
+ "material_card_type": MATERIAL_CARD_MAP[family],
351
+ "confidence_score": round(float(RNG.uniform(0.55, 0.97)), 3),
352
+ "source": RNG.choice(
353
+ ["MatWeb", "NIST MDR", "Literature", "CAE Benchmark", "Public Dataset"]
354
+ ),
355
+ }
356
+ )
357
+
358
+ df = pd.DataFrame(rows)
359
+ return df.sample(frac=1.0, random_state=42).reset_index(drop=True)
360
+
361
+
362
+ def generate_stress_strain(materials: pd.DataFrame, n_curves: int = 400) -> pd.DataFrame:
363
+ """Generate plastic stress–strain curves for a subset of materials."""
364
+ sample = materials.sample(n=min(n_curves, len(materials)), random_state=7)
365
+ curve_rows: list[dict] = []
366
+ strains = np.linspace(0, 0.25, 40)
367
+
368
+ for _, mat in sample.iterrows():
369
+ e = mat["youngs_modulus_gpa"] * 1000 # MPa
370
+ ys = mat["yield_strength_mpa"]
371
+ uts = mat["uts_mpa"]
372
+ n_hard = RNG.uniform(0.08, 0.28)
373
+ for eps in strains:
374
+ if eps * e < ys:
375
+ stress = eps * e
376
+ else:
377
+ plastic = max(eps - ys / e, 0)
378
+ stress = ys + (uts - ys) * (1 - np.exp(-plastic / n_hard))
379
+ stress = min(stress, uts * 1.05)
380
+ rate_factor = 1.0 + mat["strain_rate_sensitivity"] * np.log1p(100)
381
+ curve_rows.append(
382
+ {
383
+ "material_id": mat["material_id"],
384
+ "material_name": mat["material_name"],
385
+ "family": mat["family"],
386
+ "strain": round(float(eps), 5),
387
+ "stress_mpa": round(float(stress * rate_factor / rate_factor), 2),
388
+ "stress_high_rate_mpa": round(float(stress * rate_factor), 2),
389
+ }
390
+ )
391
+ return pd.DataFrame(curve_rows)
392
+
393
+
394
+ def generate_recommendations(materials: pd.DataFrame, n: int = 3000) -> pd.DataFrame:
395
+ """Generate crash-scenario recommendation / prediction records."""
396
+ rows: list[dict] = []
397
+ sample = materials.sample(n=min(n, len(materials) * 2), replace=True, random_state=11)
398
+
399
+ for i, (_, mat) in enumerate(sample.iterrows()):
400
+ scenario = CRASH_SCENARIOS[i % len(CRASH_SCENARIOS)]
401
+ component = COMPONENTS[i % len(COMPONENTS)]
402
+ thickness = float(RNG.uniform(0.8, 3.5))
403
+ joining = JOINING_METHODS[i % len(JOINING_METHODS)]
404
+
405
+ base = mat["crashworthiness_index"]
406
+ thickness_factor = np.clip(thickness / 2.0, 0.5, 1.6)
407
+ energy = float(base * thickness_factor * RNG.uniform(0.85, 1.15))
408
+ intrusion = float(np.clip(80 - base * 0.5 - thickness * 8 + RNG.normal(0, 5), 5, 120))
409
+ peak_force = float(mat["uts_mpa"] * thickness * 0.015 * RNG.uniform(0.8, 1.2))
410
+ cfe = float(np.clip(0.45 + base / 300 + RNG.normal(0, 0.05), 0.3, 0.95))
411
+ sea = float(mat["energy_absorption_potential"] * thickness_factor * RNG.uniform(0.9, 1.1))
412
+ crash_score = float(
413
+ np.clip(
414
+ 0.3 * energy
415
+ + 0.2 * (100 - intrusion)
416
+ + 0.15 * cfe * 100
417
+ + 0.15 * mat["lightweighting_score"]
418
+ + 0.1 * mat["cost_performance_score"]
419
+ + 0.1 * mat["sustainability_score"],
420
+ 10,
421
+ 98,
422
+ )
423
+ )
424
+ weight_reduction = float(
425
+ np.clip((3.0 - mat["density_g_cm3"]) / 3.0 * 40 + RNG.normal(0, 3), -5, 55)
426
+ )
427
+ sim_risk = float(np.clip(mat["failure_risk"] * 100 + RNG.normal(0, 5), 5, 95))
428
+
429
+ rows.append(
430
+ {
431
+ "rec_id": f"REC-{i:05d}",
432
+ "material_id": mat["material_id"],
433
+ "material_name": mat["material_name"],
434
+ "family": mat["family"],
435
+ "component": component,
436
+ "crash_scenario": scenario,
437
+ "thickness_mm": round(thickness, 2),
438
+ "joining_method": joining,
439
+ "energy_absorption_kj": round(energy, 2),
440
+ "intrusion_mm": round(intrusion, 2),
441
+ "peak_force_kn": round(peak_force, 2),
442
+ "crush_force_efficiency": round(cfe, 3),
443
+ "specific_energy_absorption": round(sea, 3),
444
+ "crash_score": round(crash_score, 2),
445
+ "weight_reduction_pct": round(weight_reduction, 2),
446
+ "cost_score": round(mat["cost_performance_score"], 2),
447
+ "sustainability_score": round(mat["sustainability_score"], 2),
448
+ "lightweighting_score": round(mat["lightweighting_score"], 2),
449
+ "simulation_risk": round(sim_risk, 2),
450
+ "failure_risk": round(mat["failure_risk"], 3),
451
+ "solver": SOLVERS[i % len(SOLVERS)],
452
+ "validation_level": RNG.choice(
453
+ [
454
+ "Coupon Test",
455
+ "Component Test",
456
+ "CAE Validation",
457
+ "Physical Crash",
458
+ "Certification Align",
459
+ ]
460
+ ),
461
+ "required_test": RNG.choice(
462
+ [
463
+ "Tensile + Strain-Rate",
464
+ "3-Point Bend",
465
+ "Drop Tower",
466
+ "Component Crush",
467
+ "Side Pole Sled",
468
+ "Full-Vehicle Barrier",
469
+ ]
470
+ ),
471
+ }
472
+ )
473
+ return pd.DataFrame(rows)
474
+
475
+
476
+ def generate_validation(recommendations: pd.DataFrame, n: int = 1500) -> pd.DataFrame:
477
+ """Generate AI vs CAE / NHTSA-style validation comparison records."""
478
+ sample = recommendations.sample(n=min(n, len(recommendations)), random_state=21)
479
+ rows: list[dict] = []
480
+ for i, (_, rec) in enumerate(sample.iterrows()):
481
+ ai = rec["crash_score"]
482
+ noise = RNG.normal(0, 4)
483
+ cae = float(np.clip(ai + noise, 5, 100))
484
+ physical = float(np.clip(cae + RNG.normal(0, 3), 5, 100))
485
+ error_ai_cae = abs(ai - cae) / max(cae, 1e-6) * 100
486
+ error_cae_phys = abs(cae - physical) / max(physical, 1e-6) * 100
487
+ rows.append(
488
+ {
489
+ "val_id": f"VAL-{i:04d}",
490
+ "rec_id": rec["rec_id"],
491
+ "material_name": rec["material_name"],
492
+ "family": rec["family"],
493
+ "component": rec["component"],
494
+ "crash_scenario": rec["crash_scenario"],
495
+ "ai_crash_score": round(ai, 2),
496
+ "cae_crash_score": round(cae, 2),
497
+ "physical_crash_score": round(physical, 2),
498
+ "ai_cae_error_pct": round(error_ai_cae, 2),
499
+ "cae_physical_error_pct": round(error_cae_phys, 2),
500
+ "nhtsa_star_proxy": int(np.clip(round(physical / 20), 1, 5)),
501
+ "standard": RNG.choice(["Euro NCAP", "FMVSS", "IIHS", "OEM Internal"]),
502
+ "pass_fail": "Pass" if error_ai_cae < 12 else "Review",
503
+ }
504
+ )
505
+ return pd.DataFrame(rows)
506
+
507
+
508
+ def generate_all(data_dir: Path | str) -> dict[str, pd.DataFrame]:
509
+ """Generate and persist all datasets."""
510
+ data_dir = Path(data_dir)
511
+ data_dir.mkdir(parents=True, exist_ok=True)
512
+
513
+ materials = generate_materials(6000)
514
+ stress = generate_stress_strain(materials, 400)
515
+ recommendations = generate_recommendations(materials, 3500)
516
+ validation = generate_validation(recommendations, 1800)
517
+
518
+ materials.to_csv(data_dir / "materials.csv", index=False)
519
+ stress.to_csv(data_dir / "stress_strain.csv", index=False)
520
+ recommendations.to_csv(data_dir / "recommendations.csv", index=False)
521
+ validation.to_csv(data_dir / "validation.csv", index=False)
522
+
523
+ return {
524
+ "materials": materials,
525
+ "stress_strain": stress,
526
+ "recommendations": recommendations,
527
+ "validation": validation,
528
+ }
529
+
530
+
531
+ if __name__ == "__main__":
532
+ out = Path(__file__).resolve().parent.parent / "data"
533
+ datasets = generate_all(out)
534
+ for name, df in datasets.items():
535
+ print(f"{name}: {len(df)} rows")
utils/visualizations.py ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Plotly visualization helpers for Crash Intelligence."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pandas as pd
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+
9
+ COLOR_SEQUENCE = [
10
+ "#0B6E4F",
11
+ "#08A045",
12
+ "#1B4965",
13
+ "#5FA8D3",
14
+ "#C44536",
15
+ "#E8871E",
16
+ "#6B4C9A",
17
+ "#2A9D8F",
18
+ "#E76F51",
19
+ "#264653",
20
+ "#F4A261",
21
+ "#457B9D",
22
+ "#9B2226",
23
+ "#005F73",
24
+ "#CA6702",
25
+ ]
26
+
27
+ LAYOUT_DEFAULTS = dict(
28
+ paper_bgcolor="rgba(255,255,255,1)",
29
+ plot_bgcolor="rgba(248,250,252,1)",
30
+ font=dict(family="Source Sans 3, Segoe UI, sans-serif", color="#1a1a1a", size=13),
31
+ margin=dict(l=50, r=30, t=50, b=50),
32
+ legend=dict(bgcolor="rgba(255,255,255,0.9)", bordercolor="#ddd", borderwidth=1),
33
+ )
34
+
35
+
36
+ def _apply_layout(fig: go.Figure, title: str, height: int = 420) -> go.Figure:
37
+ fig.update_layout(title=title, height=height, **LAYOUT_DEFAULTS)
38
+ fig.update_xaxes(showgrid=True, gridcolor="#e5e7eb", zeroline=False)
39
+ fig.update_yaxes(showgrid=True, gridcolor="#e5e7eb", zeroline=False)
40
+ return fig
41
+
42
+
43
+ def family_bar(summary: pd.DataFrame, metric: str, title: str) -> go.Figure:
44
+ fig = px.bar(
45
+ summary.sort_values(metric, ascending=True),
46
+ x=metric,
47
+ y="family",
48
+ orientation="h",
49
+ color=metric,
50
+ color_continuous_scale=["#D8F3DC", "#0B6E4F"],
51
+ labels={"family": "Material Family", metric: metric.replace("_", " ").title()},
52
+ )
53
+ return _apply_layout(fig, title, height=480)
54
+
55
+
56
+ def scatter_crash_vs_weight(materials: pd.DataFrame) -> go.Figure:
57
+ fig = px.scatter(
58
+ materials.sample(n=min(2000, len(materials)), random_state=3),
59
+ x="lightweighting_score",
60
+ y="crashworthiness_index",
61
+ color="family",
62
+ size="uts_mpa",
63
+ hover_data=["material_name", "cost_usd_kg", "sustainability_score"],
64
+ color_discrete_sequence=COLOR_SEQUENCE,
65
+ labels={
66
+ "lightweighting_score": "Lightweighting Score",
67
+ "crashworthiness_index": "Crashworthiness Index",
68
+ },
69
+ )
70
+ return _apply_layout(fig, "Crashworthiness vs Lightweighting", height=480)
71
+
72
+
73
+ def radar_materials(top: pd.DataFrame) -> go.Figure:
74
+ categories = [
75
+ "crashworthiness_index",
76
+ "lightweighting_score",
77
+ "cost_performance_score",
78
+ "sustainability_score",
79
+ "energy_absorption_potential",
80
+ ]
81
+ labels = ["Crash", "Weight", "Cost-Perf", "Sustainability", "Energy Abs."]
82
+ fig = go.Figure()
83
+ for i, (_, row) in enumerate(top.head(5).iterrows()):
84
+ values = []
85
+ for c in categories:
86
+ v = float(row[c])
87
+ if c == "energy_absorption_potential":
88
+ v = min(v * 2.0, 100)
89
+ if c == "cost_performance_score":
90
+ v = min(v * 1.5, 100)
91
+ values.append(v)
92
+ values.append(values[0])
93
+ fig.add_trace(
94
+ go.Scatterpolar(
95
+ r=values,
96
+ theta=labels + [labels[0]],
97
+ name=str(row.get("material_name", row.get("family", f"M{i}"))),
98
+ line=dict(color=COLOR_SEQUENCE[i % len(COLOR_SEQUENCE)], width=2),
99
+ fill="toself",
100
+ opacity=0.55,
101
+ )
102
+ )
103
+ fig.update_layout(
104
+ polar=dict(
105
+ bgcolor="#f8fafc",
106
+ radialaxis=dict(visible=True, range=[0, 100], gridcolor="#e5e7eb"),
107
+ angularaxis=dict(gridcolor="#e5e7eb"),
108
+ ),
109
+ title="Multi-Objective Material Comparison",
110
+ height=480,
111
+ **{k: v for k, v in LAYOUT_DEFAULTS.items() if k != "margin"},
112
+ margin=dict(l=60, r=60, t=50, b=40),
113
+ )
114
+ return fig
115
+
116
+
117
+ def stress_strain_curves(curves: pd.DataFrame, material_ids: list[str]) -> go.Figure:
118
+ fig = go.Figure()
119
+ subset = curves[curves["material_id"].isin(material_ids)]
120
+ for i, mid in enumerate(material_ids):
121
+ mdf = subset[subset["material_id"] == mid]
122
+ if mdf.empty:
123
+ continue
124
+ name = mdf["material_name"].iloc[0]
125
+ fig.add_trace(
126
+ go.Scatter(
127
+ x=mdf["strain"],
128
+ y=mdf["stress_mpa"],
129
+ mode="lines",
130
+ name=f"{name} (quasi-static)",
131
+ line=dict(color=COLOR_SEQUENCE[i % len(COLOR_SEQUENCE)], width=2.5),
132
+ )
133
+ )
134
+ fig.add_trace(
135
+ go.Scatter(
136
+ x=mdf["strain"],
137
+ y=mdf["stress_high_rate_mpa"],
138
+ mode="lines",
139
+ name=f"{name} (high-rate)",
140
+ line=dict(
141
+ color=COLOR_SEQUENCE[i % len(COLOR_SEQUENCE)],
142
+ width=2,
143
+ dash="dash",
144
+ ),
145
+ )
146
+ )
147
+ fig.update_layout(
148
+ xaxis_title="True Strain",
149
+ yaxis_title="True Stress (MPa)",
150
+ )
151
+ return _apply_layout(fig, "Stress–Strain Curves (Strain-Rate Sensitive)", height=460)
152
+
153
+
154
+ def scenario_heatmap(recommendations: pd.DataFrame) -> go.Figure:
155
+ pivot = (
156
+ recommendations.groupby(["crash_scenario", "family"])["crash_score"]
157
+ .mean()
158
+ .reset_index()
159
+ .pivot(index="crash_scenario", columns="family", values="crash_score")
160
+ )
161
+ fig = px.imshow(
162
+ pivot,
163
+ color_continuous_scale=["#F1FAEE", "#1B4965", "#0B6E4F"],
164
+ aspect="auto",
165
+ labels=dict(color="Crash Score"),
166
+ )
167
+ return _apply_layout(fig, "Avg Crash Score by Scenario × Family", height=520)
168
+
169
+
170
+ def energy_intrusion_scatter(recommendations: pd.DataFrame) -> go.Figure:
171
+ sample = recommendations.sample(n=min(1500, len(recommendations)), random_state=5)
172
+ fig = px.scatter(
173
+ sample,
174
+ x="intrusion_mm",
175
+ y="energy_absorption_kj",
176
+ color="crash_scenario",
177
+ symbol="family",
178
+ hover_data=["material_name", "component", "crash_score"],
179
+ color_discrete_sequence=COLOR_SEQUENCE,
180
+ labels={
181
+ "intrusion_mm": "Intrusion (mm)",
182
+ "energy_absorption_kj": "Energy Absorption (kJ)",
183
+ },
184
+ )
185
+ return _apply_layout(fig, "Energy Absorption vs Intrusion", height=460)
186
+
187
+
188
+ def validation_parity(validation: pd.DataFrame) -> go.Figure:
189
+ fig = go.Figure()
190
+ fig.add_trace(
191
+ go.Scatter(
192
+ x=validation["cae_crash_score"],
193
+ y=validation["ai_crash_score"],
194
+ mode="markers",
195
+ name="AI vs CAE",
196
+ marker=dict(color="#1B4965", size=7, opacity=0.55),
197
+ )
198
+ )
199
+ lims = [0, 100]
200
+ fig.add_trace(
201
+ go.Scatter(
202
+ x=lims,
203
+ y=lims,
204
+ mode="lines",
205
+ name="Ideal",
206
+ line=dict(color="#C44536", dash="dash", width=2),
207
+ )
208
+ )
209
+ fig.update_layout(xaxis_title="CAE Crash Score", yaxis_title="AI Crash Score")
210
+ return _apply_layout(fig, "AI Prediction vs CAE Validation", height=440)
211
+
212
+
213
+ def validation_error_hist(validation: pd.DataFrame) -> go.Figure:
214
+ fig = px.histogram(
215
+ validation,
216
+ x="ai_cae_error_pct",
217
+ nbins=30,
218
+ color="pass_fail",
219
+ color_discrete_map={"Pass": "#0B6E4F", "Review": "#C44536"},
220
+ labels={"ai_cae_error_pct": "AI–CAE Error (%)"},
221
+ )
222
+ return _apply_layout(fig, "AI–CAE Error Distribution", height=400)
223
+
224
+
225
+ def cost_sustain_bubble(materials: pd.DataFrame) -> go.Figure:
226
+ sample = materials.sample(n=min(1500, len(materials)), random_state=9)
227
+ fig = px.scatter(
228
+ sample,
229
+ x="cost_usd_kg",
230
+ y="sustainability_score",
231
+ size="crashworthiness_index",
232
+ color="family",
233
+ hover_data=["material_name", "density_g_cm3", "uts_mpa"],
234
+ color_discrete_sequence=COLOR_SEQUENCE,
235
+ labels={
236
+ "cost_usd_kg": "Cost (USD/kg)",
237
+ "sustainability_score": "Sustainability Score",
238
+ },
239
+ )
240
+ return _apply_layout(fig, "Cost vs Sustainability (bubble = crash score)", height=460)
241
+
242
+
243
+ def top_recommendations_bar(top: pd.DataFrame) -> go.Figure:
244
+ plot_df = top.copy()
245
+ name_col = "material_name" if "material_name" in plot_df.columns else "family"
246
+ fig = px.bar(
247
+ plot_df.sort_values("crash_score", ascending=True),
248
+ x="crash_score",
249
+ y=name_col,
250
+ color="family" if "family" in plot_df.columns else None,
251
+ orientation="h",
252
+ color_discrete_sequence=COLOR_SEQUENCE,
253
+ labels={"crash_score": "Crash Score", name_col: "Material"},
254
+ )
255
+ return _apply_layout(fig, "Top Recommended Materials", height=420)
256
+
257
+
258
+ def kpi_gauge(value: float, title: str, color: str = "#0B6E4F") -> go.Figure:
259
+ fig = go.Figure(
260
+ go.Indicator(
261
+ mode="gauge+number",
262
+ value=value,
263
+ title={"text": title, "font": {"size": 14, "color": "#1a1a1a"}},
264
+ number={"font": {"color": "#1a1a1a"}},
265
+ gauge={
266
+ "axis": {"range": [0, 100], "tickcolor": "#1a1a1a"},
267
+ "bar": {"color": color},
268
+ "bgcolor": "#f1f5f9",
269
+ "bordercolor": "#cbd5e1",
270
+ "steps": [
271
+ {"range": [0, 40], "color": "#fee2e2"},
272
+ {"range": [40, 70], "color": "#fef3c7"},
273
+ {"range": [70, 100], "color": "#dcfce7"},
274
+ ],
275
+ },
276
+ )
277
+ )
278
+ fig.update_layout(
279
+ height=220,
280
+ margin=dict(l=20, r=20, t=40, b=10),
281
+ paper_bgcolor="white",
282
+ font=dict(color="#1a1a1a"),
283
+ )
284
+ return fig