| """Synthetic public-style material and crash datasets for the Crash Intelligence platform.""" |
|
|
| from __future__ import annotations |
|
|
| from pathlib import Path |
|
|
| import numpy as np |
| import pandas as pd |
|
|
| RNG = np.random.default_rng(42) |
|
|
| MATERIAL_FAMILIES = { |
| "Mild Steel": { |
| "grades": ["AISI 1008", "AISI 1010", "DC04", "DC06", "St14"], |
| "density": (7.80, 7.87), |
| "modulus": (200, 210), |
| "yield": (140, 280), |
| "uts": (270, 420), |
| "elongation": (25, 45), |
| "failure_strain": (0.25, 0.55), |
| "cost": (0.6, 1.2), |
| "co2": (1.8, 2.5), |
| "recyclability": (0.85, 0.98), |
| "strain_rate_sens": (0.01, 0.04), |
| }, |
| "AHSS": { |
| "grades": ["DP600", "DP800", "DP1000", "TRIP780", "CP800", "MS1200"], |
| "density": (7.80, 7.85), |
| "modulus": (200, 210), |
| "yield": (350, 900), |
| "uts": (600, 1200), |
| "elongation": (8, 25), |
| "failure_strain": (0.08, 0.22), |
| "cost": (1.0, 2.2), |
| "co2": (2.0, 2.8), |
| "recyclability": (0.80, 0.95), |
| "strain_rate_sens": (0.02, 0.06), |
| }, |
| "UHSS": { |
| "grades": ["MS1500", "MS1700", "PHS1500", "PHS1800", "QP980", "QP1180"], |
| "density": (7.80, 7.85), |
| "modulus": (200, 210), |
| "yield": (900, 1500), |
| "uts": (1200, 2000), |
| "elongation": (4, 12), |
| "failure_strain": (0.04, 0.12), |
| "cost": (1.8, 3.5), |
| "co2": (2.2, 3.2), |
| "recyclability": (0.75, 0.92), |
| "strain_rate_sens": (0.015, 0.05), |
| }, |
| "Boron Steel": { |
| "grades": ["22MnB5", "30MnB5", "Usibor 1500", "Usibor 2000", "Ductibor 500"], |
| "density": (7.80, 7.85), |
| "modulus": (200, 210), |
| "yield": (1000, 1400), |
| "uts": (1400, 2000), |
| "elongation": (5, 10), |
| "failure_strain": (0.05, 0.10), |
| "cost": (2.0, 3.8), |
| "co2": (2.3, 3.4), |
| "recyclability": (0.78, 0.93), |
| "strain_rate_sens": (0.02, 0.045), |
| }, |
| "Aluminum 5xxx": { |
| "grades": ["AA5052", "AA5083", "AA5182", "AA5754", "AA5454"], |
| "density": (2.66, 2.70), |
| "modulus": (68, 72), |
| "yield": (90, 220), |
| "uts": (190, 320), |
| "elongation": (12, 30), |
| "failure_strain": (0.15, 0.35), |
| "cost": (2.5, 4.0), |
| "co2": (8.0, 12.0), |
| "recyclability": (0.90, 0.98), |
| "strain_rate_sens": (0.01, 0.03), |
| }, |
| "Aluminum 6xxx": { |
| "grades": ["AA6005", "AA6061", "AA6063", "AA6082", "AA6111"], |
| "density": (2.68, 2.71), |
| "modulus": (68, 72), |
| "yield": (150, 280), |
| "uts": (220, 340), |
| "elongation": (8, 18), |
| "failure_strain": (0.10, 0.22), |
| "cost": (2.8, 4.5), |
| "co2": (8.5, 13.0), |
| "recyclability": (0.90, 0.98), |
| "strain_rate_sens": (0.01, 0.035), |
| }, |
| "Aluminum 7xxx": { |
| "grades": ["AA7003", "AA7020", "AA7075", "AA7050", "AA7085"], |
| "density": (2.78, 2.82), |
| "modulus": (70, 73), |
| "yield": (300, 500), |
| "uts": (400, 580), |
| "elongation": (5, 12), |
| "failure_strain": (0.06, 0.14), |
| "cost": (4.0, 7.0), |
| "co2": (10.0, 16.0), |
| "recyclability": (0.85, 0.95), |
| "strain_rate_sens": (0.008, 0.025), |
| }, |
| "Magnesium": { |
| "grades": ["AZ31B", "AZ61", "AZ91", "AM60", "ZK60"], |
| "density": (1.74, 1.82), |
| "modulus": (42, 48), |
| "yield": (120, 220), |
| "uts": (200, 320), |
| "elongation": (8, 18), |
| "failure_strain": (0.08, 0.20), |
| "cost": (4.5, 8.0), |
| "co2": (15.0, 25.0), |
| "recyclability": (0.70, 0.90), |
| "strain_rate_sens": (0.02, 0.05), |
| }, |
| "CFRP": { |
| "grades": ["T700/Epoxy", "T800/Epoxy", "IM7/PEEK", "M55J/Epoxy", "AS4/Epoxy"], |
| "density": (1.50, 1.65), |
| "modulus": (70, 150), |
| "yield": (600, 1200), |
| "uts": (800, 1800), |
| "elongation": (1.2, 2.5), |
| "failure_strain": (0.012, 0.025), |
| "cost": (25, 80), |
| "co2": (20, 45), |
| "recyclability": (0.20, 0.45), |
| "strain_rate_sens": (0.005, 0.02), |
| }, |
| "GFRP": { |
| "grades": ["E-Glass/Epoxy", "S-Glass/Epoxy", "E-Glass/PP", "E-Glass/PA6", "SMC"], |
| "density": (1.80, 2.10), |
| "modulus": (20, 45), |
| "yield": (200, 450), |
| "uts": (300, 700), |
| "elongation": (1.5, 4.0), |
| "failure_strain": (0.015, 0.04), |
| "cost": (5, 18), |
| "co2": (4, 12), |
| "recyclability": (0.25, 0.50), |
| "strain_rate_sens": (0.01, 0.03), |
| }, |
| "Natural Fiber Composite": { |
| "grades": ["Flax/PP", "Hemp/PLA", "Jute/Epoxy", "Kenaf/PP", "Bamboo/Epoxy"], |
| "density": (1.20, 1.50), |
| "modulus": (8, 25), |
| "yield": (60, 150), |
| "uts": (80, 200), |
| "elongation": (2, 6), |
| "failure_strain": (0.02, 0.06), |
| "cost": (3, 10), |
| "co2": (0.5, 3.0), |
| "recyclability": (0.50, 0.80), |
| "strain_rate_sens": (0.015, 0.04), |
| }, |
| "Polymer": { |
| "grades": ["PP-GF30", "PA6-GF35", "ABS", "PC/ABS", "PBT-GF30", "TPU"], |
| "density": (0.95, 1.45), |
| "modulus": (1.5, 12), |
| "yield": (25, 120), |
| "uts": (30, 160), |
| "elongation": (5, 80), |
| "failure_strain": (0.05, 1.5), |
| "cost": (1.5, 6.0), |
| "co2": (2.0, 6.5), |
| "recyclability": (0.40, 0.85), |
| "strain_rate_sens": (0.03, 0.10), |
| }, |
| "Elastomer": { |
| "grades": ["EPDM", "NR", "SBR", "NBR", "Silicone"], |
| "density": (0.90, 1.25), |
| "modulus": (0.005, 0.05), |
| "yield": (2, 15), |
| "uts": (5, 30), |
| "elongation": (200, 600), |
| "failure_strain": (2.0, 6.0), |
| "cost": (2.0, 8.0), |
| "co2": (2.5, 7.0), |
| "recyclability": (0.15, 0.40), |
| "strain_rate_sens": (0.05, 0.15), |
| }, |
| "Structural Foam": { |
| "grades": ["EPS", "EPP", "PUR Foam", "Al Honeycomb", "PET Foam"], |
| "density": (0.03, 0.25), |
| "modulus": (0.01, 2.0), |
| "yield": (0.2, 8), |
| "uts": (0.3, 12), |
| "elongation": (5, 80), |
| "failure_strain": (0.4, 2.0), |
| "cost": (1.0, 15.0), |
| "co2": (1.5, 8.0), |
| "recyclability": (0.20, 0.70), |
| "strain_rate_sens": (0.04, 0.12), |
| }, |
| "Adhesive": { |
| "grades": ["Epoxy Structural", "PU Crash", "Acrylic", "MS Polymer", "Toughened Epoxy"], |
| "density": (1.05, 1.40), |
| "modulus": (0.5, 4.0), |
| "yield": (10, 45), |
| "uts": (15, 60), |
| "elongation": (5, 100), |
| "failure_strain": (0.05, 1.2), |
| "cost": (8, 35), |
| "co2": (3.0, 10.0), |
| "recyclability": (0.05, 0.25), |
| "strain_rate_sens": (0.02, 0.08), |
| }, |
| } |
|
|
| COMPONENTS = [ |
| "Bumper Beam", |
| "Crash Box", |
| "Front Rail", |
| "Door Intrusion Beam", |
| "B-Pillar", |
| "A-Pillar", |
| "Roof Rail", |
| "Seat Structure", |
| "Battery Enclosure", |
| "Underbody Shield", |
| "Hood Inner", |
| "Crossmember", |
| ] |
|
|
| CRASH_SCENARIOS = [ |
| "Frontal Crash", |
| "Side Impact", |
| "Rear Impact", |
| "Pole Impact", |
| "Pedestrian Impact", |
| "Battery Pack Crash", |
| "Bumper Beam Crash", |
| "Door Intrusion", |
| "Seat Structure Crash", |
| "BIW Crash", |
| "EV Underbody Protection", |
| ] |
|
|
| JOINING_METHODS = [ |
| "Spot Weld", |
| "Laser Weld", |
| "MIG Weld", |
| "SPR (Self-Piercing Rivet)", |
| "Structural Adhesive", |
| "Hybrid Weld-Bond", |
| "Bolt Fastened", |
| "FDS (Flow Drill Screw)", |
| ] |
|
|
| SOLVERS = ["LS-DYNA", "Abaqus Explicit", "PAM-CRASH", "Radioss"] |
|
|
| MATERIAL_CARD_MAP = { |
| "Mild Steel": "MAT_024", |
| "AHSS": "MAT_024", |
| "UHSS": "MAT_024", |
| "Boron Steel": "MAT_024", |
| "Aluminum 5xxx": "MAT_024", |
| "Aluminum 6xxx": "MAT_024", |
| "Aluminum 7xxx": "MAT_024", |
| "Magnesium": "MAT_024", |
| "CFRP": "MAT_054 / Composite Damage", |
| "GFRP": "MAT_054 / Composite Damage", |
| "Natural Fiber Composite": "MAT_054 / Composite Damage", |
| "Polymer": "MAT_187 / SAMP-1", |
| "Elastomer": "MAT_077 / Hyperelastic", |
| "Structural Foam": "MAT_063 / Crushable Foam", |
| "Adhesive": "MAT_240 / Cohesive Zone", |
| } |
|
|
|
|
| def _sample_range(bounds: tuple[float, float], n: int) -> np.ndarray: |
| lo, hi = bounds |
| return RNG.uniform(lo, hi, n) |
|
|
|
|
| def generate_materials(n_records: int = 6000) -> pd.DataFrame: |
| """Generate a large public-style automotive crash materials database.""" |
| families = list(MATERIAL_FAMILIES.keys()) |
| rows: list[dict] = [] |
|
|
| per_family = max(1, n_records // len(families)) |
| for family in families: |
| spec = MATERIAL_FAMILIES[family] |
| n = per_family |
| grades = spec["grades"] |
| for i in range(n): |
| grade = grades[i % len(grades)] |
| density = float(_sample_range(spec["density"], 1)[0]) |
| modulus = float(_sample_range(spec["modulus"], 1)[0]) |
| yield_s = float(_sample_range(spec["yield"], 1)[0]) |
| uts = float(_sample_range(spec["uts"], 1)[0]) |
| if uts < yield_s: |
| uts = yield_s * RNG.uniform(1.05, 1.35) |
| elong = float(_sample_range(spec["elongation"], 1)[0]) |
| fail = float(_sample_range(spec["failure_strain"], 1)[0]) |
| cost = float(_sample_range(spec["cost"], 1)[0]) |
| co2 = float(_sample_range(spec["co2"], 1)[0]) |
| recycl = float(_sample_range(spec["recyclability"], 1)[0]) |
| srs = float(_sample_range(spec["strain_rate_sens"], 1)[0]) |
|
|
| specific_strength = uts / density |
| specific_stiffness = modulus / density |
| energy_abs_potential = 0.5 * (yield_s + uts) * fail / density |
| ductility_index = elong / max(uts / 100.0, 1e-6) |
| crash_index = ( |
| 0.35 * (energy_abs_potential / 50.0) |
| + 0.25 * (specific_strength / 200.0) |
| + 0.20 * min(fail * 5.0, 1.0) |
| + 0.20 * (1.0 / (1.0 + cost / 10.0)) |
| ) |
| crash_index = float(np.clip(crash_index * 100, 5, 98)) |
| strength_weight = specific_strength |
| failure_risk = float(np.clip(1.0 - fail * 2.5 + srs * 2.0, 0.05, 0.95)) |
| cost_perf = float(np.clip(crash_index / (cost + 0.5), 1, 80)) |
| sustain = float( |
| np.clip( |
| 100 * recycl * (1.0 / (1.0 + co2 / 10.0)) * (1.0 / (1.0 + density / 5.0)), |
| 5, |
| 98, |
| ) |
| ) |
| lightweight = float(np.clip(100 * (1.0 - density / 8.0) * (specific_strength / 300.0), 5, 98)) |
|
|
| rows.append( |
| { |
| "material_id": f"{family[:3].upper()}-{i:04d}", |
| "material_name": f"{grade}-{i % 100:02d}", |
| "family": family, |
| "grade": grade, |
| "density_g_cm3": round(density, 3), |
| "youngs_modulus_gpa": round(modulus, 2), |
| "yield_strength_mpa": round(yield_s, 1), |
| "uts_mpa": round(uts, 1), |
| "elongation_pct": round(elong, 2), |
| "failure_strain": round(fail, 4), |
| "strain_rate_sensitivity": round(srs, 4), |
| "cost_usd_kg": round(cost, 2), |
| "co2_kg_kg": round(co2, 2), |
| "recyclability": round(recycl, 3), |
| "specific_strength": round(specific_strength, 2), |
| "specific_stiffness": round(specific_stiffness, 2), |
| "energy_absorption_potential": round(energy_abs_potential, 3), |
| "ductility_index": round(ductility_index, 3), |
| "crashworthiness_index": round(crash_index, 2), |
| "strength_to_weight": round(strength_weight, 2), |
| "failure_risk": round(failure_risk, 3), |
| "cost_performance_score": round(cost_perf, 2), |
| "sustainability_score": round(sustain, 2), |
| "lightweighting_score": round(lightweight, 2), |
| "material_card_type": MATERIAL_CARD_MAP[family], |
| "confidence_score": round(float(RNG.uniform(0.55, 0.97)), 3), |
| "source": RNG.choice( |
| ["MatWeb", "NIST MDR", "Literature", "CAE Benchmark", "Public Dataset"] |
| ), |
| } |
| ) |
|
|
| df = pd.DataFrame(rows) |
| return df.sample(frac=1.0, random_state=42).reset_index(drop=True) |
|
|
|
|
| def generate_stress_strain(materials: pd.DataFrame, n_curves: int = 400) -> pd.DataFrame: |
| """Generate plastic stress–strain curves for a subset of materials.""" |
| sample = materials.sample(n=min(n_curves, len(materials)), random_state=7) |
| curve_rows: list[dict] = [] |
| strains = np.linspace(0, 0.25, 40) |
|
|
| for _, mat in sample.iterrows(): |
| e = mat["youngs_modulus_gpa"] * 1000 |
| ys = mat["yield_strength_mpa"] |
| uts = mat["uts_mpa"] |
| n_hard = RNG.uniform(0.08, 0.28) |
| for eps in strains: |
| if eps * e < ys: |
| stress = eps * e |
| else: |
| plastic = max(eps - ys / e, 0) |
| stress = ys + (uts - ys) * (1 - np.exp(-plastic / n_hard)) |
| stress = min(stress, uts * 1.05) |
| rate_factor = 1.0 + mat["strain_rate_sensitivity"] * np.log1p(100) |
| curve_rows.append( |
| { |
| "material_id": mat["material_id"], |
| "material_name": mat["material_name"], |
| "family": mat["family"], |
| "strain": round(float(eps), 5), |
| "stress_mpa": round(float(stress * rate_factor / rate_factor), 2), |
| "stress_high_rate_mpa": round(float(stress * rate_factor), 2), |
| } |
| ) |
| return pd.DataFrame(curve_rows) |
|
|
|
|
| def generate_recommendations(materials: pd.DataFrame, n: int = 3000) -> pd.DataFrame: |
| """Generate crash-scenario recommendation / prediction records.""" |
| rows: list[dict] = [] |
| sample = materials.sample(n=min(n, len(materials) * 2), replace=True, random_state=11) |
|
|
| for i, (_, mat) in enumerate(sample.iterrows()): |
| scenario = CRASH_SCENARIOS[i % len(CRASH_SCENARIOS)] |
| component = COMPONENTS[i % len(COMPONENTS)] |
| thickness = float(RNG.uniform(0.8, 3.5)) |
| joining = JOINING_METHODS[i % len(JOINING_METHODS)] |
|
|
| base = mat["crashworthiness_index"] |
| thickness_factor = np.clip(thickness / 2.0, 0.5, 1.6) |
| energy = float(base * thickness_factor * RNG.uniform(0.85, 1.15)) |
| intrusion = float(np.clip(80 - base * 0.5 - thickness * 8 + RNG.normal(0, 5), 5, 120)) |
| peak_force = float(mat["uts_mpa"] * thickness * 0.015 * RNG.uniform(0.8, 1.2)) |
| cfe = float(np.clip(0.45 + base / 300 + RNG.normal(0, 0.05), 0.3, 0.95)) |
| sea = float(mat["energy_absorption_potential"] * thickness_factor * RNG.uniform(0.9, 1.1)) |
| crash_score = float( |
| np.clip( |
| 0.3 * energy |
| + 0.2 * (100 - intrusion) |
| + 0.15 * cfe * 100 |
| + 0.15 * mat["lightweighting_score"] |
| + 0.1 * mat["cost_performance_score"] |
| + 0.1 * mat["sustainability_score"], |
| 10, |
| 98, |
| ) |
| ) |
| weight_reduction = float( |
| np.clip((3.0 - mat["density_g_cm3"]) / 3.0 * 40 + RNG.normal(0, 3), -5, 55) |
| ) |
| sim_risk = float(np.clip(mat["failure_risk"] * 100 + RNG.normal(0, 5), 5, 95)) |
|
|
| rows.append( |
| { |
| "rec_id": f"REC-{i:05d}", |
| "material_id": mat["material_id"], |
| "material_name": mat["material_name"], |
| "family": mat["family"], |
| "component": component, |
| "crash_scenario": scenario, |
| "thickness_mm": round(thickness, 2), |
| "joining_method": joining, |
| "energy_absorption_kj": round(energy, 2), |
| "intrusion_mm": round(intrusion, 2), |
| "peak_force_kn": round(peak_force, 2), |
| "crush_force_efficiency": round(cfe, 3), |
| "specific_energy_absorption": round(sea, 3), |
| "crash_score": round(crash_score, 2), |
| "weight_reduction_pct": round(weight_reduction, 2), |
| "cost_score": round(mat["cost_performance_score"], 2), |
| "sustainability_score": round(mat["sustainability_score"], 2), |
| "lightweighting_score": round(mat["lightweighting_score"], 2), |
| "simulation_risk": round(sim_risk, 2), |
| "failure_risk": round(mat["failure_risk"], 3), |
| "solver": SOLVERS[i % len(SOLVERS)], |
| "validation_level": RNG.choice( |
| [ |
| "Coupon Test", |
| "Component Test", |
| "CAE Validation", |
| "Physical Crash", |
| "Certification Align", |
| ] |
| ), |
| "required_test": RNG.choice( |
| [ |
| "Tensile + Strain-Rate", |
| "3-Point Bend", |
| "Drop Tower", |
| "Component Crush", |
| "Side Pole Sled", |
| "Full-Vehicle Barrier", |
| ] |
| ), |
| } |
| ) |
| return pd.DataFrame(rows) |
|
|
|
|
| def generate_validation(recommendations: pd.DataFrame, n: int = 1500) -> pd.DataFrame: |
| """Generate AI vs CAE / NHTSA-style validation comparison records.""" |
| sample = recommendations.sample(n=min(n, len(recommendations)), random_state=21) |
| rows: list[dict] = [] |
| for i, (_, rec) in enumerate(sample.iterrows()): |
| ai = rec["crash_score"] |
| noise = RNG.normal(0, 4) |
| cae = float(np.clip(ai + noise, 5, 100)) |
| physical = float(np.clip(cae + RNG.normal(0, 3), 5, 100)) |
| error_ai_cae = abs(ai - cae) / max(cae, 1e-6) * 100 |
| error_cae_phys = abs(cae - physical) / max(physical, 1e-6) * 100 |
| rows.append( |
| { |
| "val_id": f"VAL-{i:04d}", |
| "rec_id": rec["rec_id"], |
| "material_name": rec["material_name"], |
| "family": rec["family"], |
| "component": rec["component"], |
| "crash_scenario": rec["crash_scenario"], |
| "ai_crash_score": round(ai, 2), |
| "cae_crash_score": round(cae, 2), |
| "physical_crash_score": round(physical, 2), |
| "ai_cae_error_pct": round(error_ai_cae, 2), |
| "cae_physical_error_pct": round(error_cae_phys, 2), |
| "nhtsa_star_proxy": int(np.clip(round(physical / 20), 1, 5)), |
| "standard": RNG.choice(["Euro NCAP", "FMVSS", "IIHS", "OEM Internal"]), |
| "pass_fail": "Pass" if error_ai_cae < 12 else "Review", |
| } |
| ) |
| return pd.DataFrame(rows) |
|
|
|
|
| def generate_all(data_dir: Path | str) -> dict[str, pd.DataFrame]: |
| """Generate and persist all datasets.""" |
| data_dir = Path(data_dir) |
| data_dir.mkdir(parents=True, exist_ok=True) |
|
|
| materials = generate_materials(6000) |
| stress = generate_stress_strain(materials, 400) |
| recommendations = generate_recommendations(materials, 3500) |
| validation = generate_validation(recommendations, 1800) |
|
|
| materials.to_csv(data_dir / "materials.csv", index=False) |
| stress.to_csv(data_dir / "stress_strain.csv", index=False) |
| recommendations.to_csv(data_dir / "recommendations.csv", index=False) |
| validation.to_csv(data_dir / "validation.csv", index=False) |
|
|
| return { |
| "materials": materials, |
| "stress_strain": stress, |
| "recommendations": recommendations, |
| "validation": validation, |
| } |
|
|
|
|
| if __name__ == "__main__": |
| out = Path(__file__).resolve().parent.parent / "data" |
| datasets = generate_all(out) |
| for name, df in datasets.items(): |
| print(f"{name}: {len(df)} rows") |
|
|