| """Crash Intelligence — AI-powered automotive crash material platform.""" |
|
|
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import pandas as pd |
| import plotly.graph_objects as go |
| import streamlit as st |
|
|
| from utils.calculations import ( |
| available_families, |
| card_to_text, |
| family_summary, |
| generate_material_card, |
| rank_materials, |
| recommend_for_scenario, |
| scenario_kpi, |
| ) |
| from utils.data_generator import ( |
| COMPONENTS, |
| CRASH_SCENARIOS, |
| JOINING_METHODS, |
| SOLVERS, |
| generate_all, |
| ) |
| from utils.visualizations import ( |
| cost_sustain_bubble, |
| energy_intrusion_scatter, |
| family_bar, |
| kpi_gauge, |
| radar_materials, |
| scatter_crash_vs_weight, |
| scenario_heatmap, |
| stress_strain_curves, |
| top_recommendations_bar, |
| validation_error_hist, |
| validation_parity, |
| ) |
|
|
| DATA_DIR = Path(__file__).resolve().parent / "data" |
|
|
| st.set_page_config( |
| page_title="Crash Intelligence Platform", |
| page_icon="🛡️", |
| layout="wide", |
| initial_sidebar_state="expanded", |
| ) |
|
|
| st.markdown( |
| """ |
| <style> |
| @import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:wght@400;600;700&family=IBM+Plex+Sans:wght@500;600&display=swap'); |
| |
| html, body, [class*="css"] { |
| font-family: 'Source Sans 3', 'Segoe UI', sans-serif; |
| color: #0f172a; |
| } |
| .block-container { padding-top: 1.2rem; padding-bottom: 2rem; max-width: 1400px; } |
| h1, h2, h3 { font-family: 'IBM Plex Sans', sans-serif !important; color: #0B3D2E !important; } |
| div[data-testid="stMetricValue"] { font-size: 1.6rem; color: #0B6E4F; } |
| div[data-testid="stMetricLabel"] { color: #334155; } |
| section[data-testid="stSidebar"] { |
| background: linear-gradient(180deg, #0B3D2E 0%, #1B4965 100%); |
| } |
| section[data-testid="stSidebar"] * { color: #f8fafc !important; } |
| section[data-testid="stSidebar"] .stSelectbox label, |
| section[data-testid="stSidebar"] .stMultiSelect label, |
| section[data-testid="stSidebar"] .stSlider label { |
| color: #e2e8f0 !important; |
| } |
| .hero { |
| background: linear-gradient(120deg, #0B3D2E 0%, #1B4965 55%, #5FA8D3 100%); |
| color: #ffffff; |
| padding: 1.4rem 1.6rem; |
| border-radius: 12px; |
| margin-bottom: 1rem; |
| } |
| .hero h1 { color: #ffffff !important; margin: 0 0 0.35rem 0; font-size: 1.9rem; } |
| .hero p { color: #e2e8f0; margin: 0; font-size: 1.02rem; } |
| .card-box { |
| background: #ffffff; |
| border: 1px solid #e2e8f0; |
| border-left: 4px solid #0B6E4F; |
| border-radius: 8px; |
| padding: 0.9rem 1rem; |
| margin-bottom: 0.6rem; |
| color: #0f172a; |
| } |
| .stTabs [data-baseweb="tab"] { color: #0f172a; font-weight: 600; } |
| .stDataFrame { color: #0f172a; } |
| </style> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
|
|
| @st.cache_data(show_spinner="Loading crash material datasets…") |
| def load_datasets() -> dict[str, pd.DataFrame]: |
| materials_path = DATA_DIR / "materials.csv" |
| if not materials_path.exists(): |
| generate_all(DATA_DIR) |
| return { |
| "materials": pd.read_csv(DATA_DIR / "materials.csv"), |
| "stress_strain": pd.read_csv(DATA_DIR / "stress_strain.csv"), |
| "recommendations": pd.read_csv(DATA_DIR / "recommendations.csv"), |
| "validation": pd.read_csv(DATA_DIR / "validation.csv"), |
| } |
|
|
|
|
| def render_hero() -> None: |
| st.markdown( |
| """ |
| <div class="hero"> |
| <h1>Crash Intelligence Platform</h1> |
| <p> |
| AI-powered material recommendation, prediction, and CAE card generation for |
| automotive crash-performance applications across metals, composites, polymers, foams, and adhesives. |
| </p> |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
|
|
| def main() -> None: |
| data = load_datasets() |
| materials = data["materials"] |
| stress = data["stress_strain"] |
| recommendations = data["recommendations"] |
| validation = data["validation"] |
|
|
| render_hero() |
|
|
| families = available_families() |
| with st.sidebar: |
| st.markdown("### Filters & Targets") |
| selected_families = st.multiselect( |
| "Material families", |
| options=families, |
| default=families[:8], |
| ) |
| scenario = st.selectbox("Crash scenario", CRASH_SCENARIOS, index=0) |
| component = st.selectbox("Vehicle component", COMPONENTS, index=8) |
| max_cost = st.slider("Max cost (USD/kg)", 1.0, 80.0, 40.0, 1.0) |
| min_uts = st.slider("Min UTS (MPa)", 20, 1800, 200, 20) |
| max_density = st.slider("Max density (g/cm³)", 0.1, 8.0, 8.0, 0.1) |
| st.markdown("---") |
| st.markdown("### Multi-objective weights") |
| w_crash = st.slider("Crash performance", 0.0, 1.0, 0.30, 0.05) |
| w_weight = st.slider("Lightweighting", 0.0, 1.0, 0.20, 0.05) |
| w_cost = st.slider("Cost performance", 0.0, 1.0, 0.20, 0.05) |
| w_sust = st.slider("Sustainability", 0.0, 1.0, 0.15, 0.05) |
| w_fail = st.slider("Low failure risk", 0.0, 1.0, 0.15, 0.05) |
| weights = { |
| "crash": w_crash, |
| "weight": w_weight, |
| "cost": w_cost, |
| "sustainability": w_sust, |
| "failure": w_fail, |
| } |
| st.markdown("---") |
| st.caption( |
| f"Database: {len(materials):,} materials · " |
| f"{len(recommendations):,} scenario predictions · " |
| f"{len(validation):,} validation pairs" |
| ) |
|
|
| filt = materials[materials["family"].isin(selected_families)].copy() |
| if filt.empty: |
| st.warning("No materials match the selected families. Expand the family filter.") |
| return |
|
|
| filt = filt[ |
| (filt["cost_usd_kg"] <= max_cost) |
| & (filt["uts_mpa"] >= min_uts) |
| & (filt["density_g_cm3"] <= max_density) |
| ] |
| if filt.empty: |
| st.warning("No materials match the current property filters. Relax cost / UTS / density limits.") |
| return |
|
|
| rec_filt = recommendations[recommendations["family"].isin(selected_families)] |
|
|
| ranked = rank_materials( |
| filt, |
| families=selected_families, |
| max_cost=max_cost, |
| min_uts=min_uts, |
| max_density=max_density, |
| weights=weights, |
| top_n=8, |
| ) |
|
|
| tabs = st.tabs( |
| [ |
| "Overview", |
| "Material Explorer", |
| "Crash Scenario AI", |
| "Compare & Rank", |
| "Material Cards", |
| "Validation", |
| "Data Library", |
| ] |
| ) |
|
|
| |
| with tabs[0]: |
| st.subheader("Platform KPIs") |
| c1, c2, c3, c4, c5 = st.columns(5) |
| c1.metric("Materials", f"{len(filt):,}") |
| c2.metric("Avg Crash Index", f"{filt['crashworthiness_index'].mean():.1f}") |
| c3.metric("Avg Energy Potential", f"{filt['energy_absorption_potential'].mean():.2f}") |
| c4.metric("Avg Sustainability", f"{filt['sustainability_score'].mean():.1f}") |
| c5.metric("AI–CAE Pass Rate", f"{(validation['pass_fail']=='Pass').mean()*100:.0f}%") |
|
|
| g1, g2, g3 = st.columns(3) |
| with g1: |
| st.plotly_chart( |
| kpi_gauge(float(filt["crashworthiness_index"].mean()), "Crashworthiness", "#0B6E4F"), |
| use_container_width=True, |
| ) |
| with g2: |
| st.plotly_chart( |
| kpi_gauge(float(filt["lightweighting_score"].mean()), "Lightweighting", "#1B4965"), |
| use_container_width=True, |
| ) |
| with g3: |
| st.plotly_chart( |
| kpi_gauge(float(filt["sustainability_score"].mean()), "Sustainability", "#2A9D8F"), |
| use_container_width=True, |
| ) |
|
|
| st.markdown("#### Family performance & trade-offs") |
| summary = family_summary(filt) |
| r1, r2 = st.columns(2) |
| with r1: |
| st.plotly_chart( |
| family_bar(summary, "crashworthiness_index", "Crashworthiness by Family"), |
| use_container_width=True, |
| ) |
| with r2: |
| st.plotly_chart(scatter_crash_vs_weight(filt), use_container_width=True) |
|
|
| st.plotly_chart(scenario_heatmap(rec_filt), use_container_width=True) |
|
|
| st.markdown( |
| """ |
| <div class="card-box"> |
| <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> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| |
| with tabs[1]: |
| st.subheader("Material Data Explorer") |
| st.markdown( |
| "Browse standardized mechanical, cost, and sustainability properties across automotive crash materials." |
| ) |
| m1, m2 = st.columns(2) |
| with m1: |
| st.plotly_chart( |
| family_bar(summary, "energy_absorption_potential", "Energy Absorption Potential"), |
| use_container_width=True, |
| ) |
| with m2: |
| st.plotly_chart(cost_sustain_bubble(filt), use_container_width=True) |
|
|
| curve_options = ( |
| stress[stress["family"].isin(selected_families)][["material_id", "material_name", "family"]] |
| .drop_duplicates() |
| .head(80) |
| ) |
| if not curve_options.empty: |
| pick = st.multiselect( |
| "Select materials for stress–strain curves", |
| options=curve_options["material_id"].tolist(), |
| default=curve_options["material_id"].tolist()[:3], |
| format_func=lambda mid: ( |
| f"{curve_options.loc[curve_options.material_id==mid, 'material_name'].iloc[0]} " |
| f"({curve_options.loc[curve_options.material_id==mid, 'family'].iloc[0]})" |
| ), |
| ) |
| if pick: |
| st.plotly_chart(stress_strain_curves(stress, pick), use_container_width=True) |
|
|
| st.dataframe( |
| filt[ |
| [ |
| "material_name", |
| "family", |
| "density_g_cm3", |
| "youngs_modulus_gpa", |
| "yield_strength_mpa", |
| "uts_mpa", |
| "elongation_pct", |
| "failure_strain", |
| "cost_usd_kg", |
| "co2_kg_kg", |
| "crashworthiness_index", |
| "sustainability_score", |
| "source", |
| "confidence_score", |
| ] |
| ].sort_values("crashworthiness_index", ascending=False), |
| use_container_width=True, |
| height=360, |
| ) |
|
|
| |
| with tabs[2]: |
| st.subheader("Crash Scenario Intelligence") |
| st.markdown( |
| f"Recommendations for **{scenario}** on **{component}** using multi-objective AI ranking." |
| ) |
| top_rec = recommend_for_scenario( |
| filt, |
| rec_filt, |
| scenario=scenario, |
| component=component, |
| families=selected_families, |
| top_n=5, |
| ) |
| if top_rec.empty: |
| st.info("No recommendations available for this combination.") |
| else: |
| k1, k2, k3, k4 = st.columns(4) |
| k1.metric("Top Crash Score", f"{top_rec['crash_score'].iloc[0]:.1f}") |
| k2.metric( |
| "Best Weight Reduction", |
| f"{top_rec.get('weight_reduction_pct', pd.Series([0])).iloc[0]:.1f}%", |
| ) |
| k3.metric("Top Material", str(top_rec["material_name"].iloc[0])) |
| k4.metric("Family", str(top_rec["family"].iloc[0])) |
|
|
| st.plotly_chart(top_recommendations_bar(top_rec), use_container_width=True) |
| c_a, c_b = st.columns(2) |
| with c_a: |
| st.plotly_chart(energy_intrusion_scatter(rec_filt), use_container_width=True) |
| with c_b: |
| sk = scenario_kpi(rec_filt) |
| st.plotly_chart( |
| family_bar( |
| sk.rename(columns={"crash_scenario": "family", "avg_crash_score": "crashworthiness_index"}), |
| "crashworthiness_index", |
| "Average Crash Score by Scenario", |
| ), |
| use_container_width=True, |
| ) |
|
|
| st.markdown("#### Top 5 recommendations") |
| display_cols = [ |
| c |
| for c in [ |
| "material_name", |
| "family", |
| "thickness_mm", |
| "joining_method", |
| "crash_score", |
| "energy_absorption_kj", |
| "intrusion_mm", |
| "peak_force_kn", |
| "crush_force_efficiency", |
| "weight_reduction_pct", |
| "cost_score", |
| "sustainability_score", |
| "simulation_risk", |
| ] |
| if c in top_rec.columns |
| ] |
| st.dataframe(top_rec[display_cols], use_container_width=True) |
|
|
| st.markdown("#### Suggested next steps") |
| best = top_rec.iloc[0] |
| join = best.get("joining_method", JOINING_METHODS[0]) |
| thick = best.get("thickness_mm", 2.0) |
| st.markdown( |
| f""" |
| <div class="card-box"> |
| <strong>Recommended action:</strong> Evaluate <em>{best['material_name']}</em> |
| ({best['family']}) at ~{thick} mm with <em>{join}</em> joining. |
| Expected crash score {best['crash_score']:.1f}. |
| Run component-level {scenario.lower()} CAE before physical validation. |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| |
| with tabs[3]: |
| st.subheader("Material Comparison & Ranking") |
| if ranked.empty: |
| st.info("No ranked materials under current constraints.") |
| else: |
| st.plotly_chart(radar_materials(ranked), use_container_width=True) |
| left, right = st.columns(2) |
| with left: |
| st.plotly_chart( |
| family_bar( |
| ranked.rename(columns={"material_name": "family", "mo_score": "crashworthiness_index"})[ |
| ["family", "crashworthiness_index"] |
| ], |
| "crashworthiness_index", |
| "Multi-Objective Score (Top Materials)", |
| ), |
| use_container_width=True, |
| ) |
| with right: |
| st.dataframe( |
| ranked[ |
| [ |
| "material_name", |
| "family", |
| "mo_score", |
| "crashworthiness_index", |
| "lightweighting_score", |
| "cost_performance_score", |
| "sustainability_score", |
| "failure_risk", |
| "uts_mpa", |
| "density_g_cm3", |
| "cost_usd_kg", |
| ] |
| ], |
| use_container_width=True, |
| height=420, |
| ) |
|
|
| |
| with tabs[4]: |
| st.subheader("CAE Material Card Generator") |
| st.markdown( |
| "Generate draft solver-ready material cards including elastic modulus, yield, " |
| "plastic curve, strain-rate sensitivity, failure strain, and confidence score." |
| ) |
| card_mat_name = st.selectbox( |
| "Select material", |
| options=ranked["material_name"].tolist() if not ranked.empty else filt["material_name"].head(50).tolist(), |
| ) |
| solver = st.selectbox("Target solver", SOLVERS) |
| mat_row = filt[filt["material_name"] == card_mat_name] |
| if mat_row.empty and not ranked.empty: |
| mat_row = ranked[ranked["material_name"] == card_mat_name] |
| if mat_row.empty: |
| mat_row = materials[materials["material_name"] == card_mat_name] |
|
|
| if not mat_row.empty: |
| material = mat_row.iloc[0] |
| card = generate_material_card(material, solver=solver) |
| text = card_to_text(card) |
|
|
| mc1, mc2, mc3, mc4 = st.columns(4) |
| mc1.metric("Card Type", card["card_type"].split()[0]) |
| mc2.metric("Yield (MPa)", f"{card['yield_strength_mpa']:.0f}") |
| mc3.metric("Failure Strain", f"{card['failure_strain']:.3f}") |
| mc4.metric("Confidence", f"{card['confidence_score']:.2f}") |
|
|
| col_l, col_r = st.columns([1.1, 0.9]) |
| with col_l: |
| st.code(text, language="text") |
| st.download_button( |
| "Download material card", |
| data=text, |
| file_name=f"{material['material_name']}_{solver.replace(' ', '_')}.k", |
| mime="text/plain", |
| ) |
| with col_r: |
| curve_id = material["material_id"] if "material_id" in material.index else None |
| if curve_id and curve_id in stress["material_id"].values: |
| st.plotly_chart( |
| stress_strain_curves(stress, [curve_id]), |
| use_container_width=True, |
| ) |
| else: |
| fig = go.Figure() |
| fig.add_trace( |
| go.Scatter( |
| x=card["plastic_curve_strain"], |
| y=card["plastic_curve_stress_mpa"], |
| mode="lines+markers", |
| line=dict(color="#0B6E4F", width=3), |
| name="Plastic curve", |
| ) |
| ) |
| fig.update_layout( |
| title="Draft Plastic Curve", |
| xaxis_title="Plastic Strain", |
| yaxis_title="Stress (MPa)", |
| height=400, |
| paper_bgcolor="white", |
| plot_bgcolor="#f8fafc", |
| font=dict(color="#1a1a1a"), |
| ) |
| st.plotly_chart(fig, use_container_width=True) |
|
|
| st.markdown( |
| f""" |
| <div class="card-box"> |
| <strong>Validation status:</strong> {card['validation_status']}<br/> |
| <strong>Damage model:</strong> {card['damage_evolution']}<br/> |
| <strong>Temperature:</strong> {card['temperature_dependency']} |
| </div> |
| """, |
| unsafe_allow_html=True, |
| ) |
|
|
| |
| with tabs[5]: |
| st.subheader("Validation Workflow") |
| st.markdown( |
| "Compare AI predictions with CAE and physical crash proxies aligned to Euro NCAP / FMVSS / IIHS." |
| ) |
| val = validation[validation["family"].isin(selected_families)] |
| v1, v2, v3, v4 = st.columns(4) |
| v1.metric("Validation pairs", f"{len(val):,}") |
| v2.metric("Mean AI–CAE error", f"{val['ai_cae_error_pct'].mean():.1f}%") |
| v3.metric("Pass rate", f"{(val['pass_fail']=='Pass').mean()*100:.0f}%") |
| v4.metric("Mean NHTSA-star proxy", f"{val['nhtsa_star_proxy'].mean():.1f}") |
|
|
| vc1, vc2 = st.columns(2) |
| with vc1: |
| st.plotly_chart(validation_parity(val), use_container_width=True) |
| with vc2: |
| st.plotly_chart(validation_error_hist(val), use_container_width=True) |
|
|
| st.markdown("#### Validation ladder") |
| levels = [ |
| ("Coupon tests", "Tensile, compression, shear, strain-rate, fracture"), |
| ("Component tests", "Bumper beam, crash box, rail, door beam, battery enclosure"), |
| ("CAE validation", "Compare AI prediction with LS-DYNA / Abaqus / PAM-CRASH"), |
| ("Physical crash", "Compare simulation with crash-test measurements"), |
| ("Certification", "Euro NCAP, FMVSS, IIHS, OEM internal standards"), |
| ] |
| for title, desc in levels: |
| st.markdown( |
| f'<div class="card-box"><strong>{title}:</strong> {desc}</div>', |
| unsafe_allow_html=True, |
| ) |
|
|
| st.dataframe( |
| val.sort_values("ai_cae_error_pct").head(200), |
| use_container_width=True, |
| height=320, |
| ) |
|
|
| |
| with tabs[6]: |
| st.subheader("Data Library & Export") |
| st.markdown( |
| "Public-style material and crash datasets used by the ranking and card-generation engines." |
| ) |
| dataset_choice = st.selectbox( |
| "Dataset", |
| ["materials", "recommendations", "validation", "stress_strain"], |
| ) |
| export_df = data[dataset_choice] |
| if dataset_choice != "stress_strain": |
| if "family" in export_df.columns: |
| export_df = export_df[export_df["family"].isin(selected_families)] |
| st.dataframe(export_df.head(500), use_container_width=True, height=400) |
| st.download_button( |
| f"Download {dataset_choice}.csv", |
| data=export_df.to_csv(index=False), |
| file_name=f"{dataset_choice}.csv", |
| mime="text/csv", |
| ) |
|
|
| st.markdown("---") |
| st.caption( |
| "Crash Intelligence Platform · Prototype powered by public-style material & crash databases · " |
| "For OEM production use, calibrate with supplier cards, high strain-rate tests, and full-vehicle CAE." |
| ) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|