Upload folder using huggingface_hub
Browse files- Dockerfile +14 -0
- README.md +29 -5
- app.py +580 -0
- data/materials.csv +0 -0
- data/recommendations.csv +0 -0
- data/stress_strain.csv +0 -0
- data/validation.csv +0 -0
- index.html +67 -1
- requirements.txt +4 -0
- utils/__init__.py +1 -0
- utils/calculations.py +215 -0
- utils/data_generator.py +535 -0
- utils/visualizations.py +284 -0
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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: static
|
| 7 |
pinned: false
|
|
|
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|