""" FEA Surrogate — Hugging Face Spaces Entry Point This demo runs the analytical solvers (Euler-Bernoulli beam theory, Kirchhoff plate theory, Lame equations) that form the ground-truth training data for the neural surrogate model. **Analytical mode** — Fast, exact closed-form solutions for 10 classical structural problems. No training required, runs instantly on CPU. **Neural mode** — Uses a trained PI-ResMLP deep ensemble with uncertainty quantification. Requires GPU training. Coming soon once NYU Torch supercomputer access is activated. """ from __future__ import annotations import os import sys import time from typing import Any # Ensure the project root is on sys.path for absolute imports _PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__)) if _PROJECT_ROOT not in sys.path: sys.path.insert(0, _PROJECT_ROOT) import gradio as gr from src.app.materials import MATERIAL_NAMES, MATERIAL_PRESETS from src.app.visualizations import ( create_beam_deformation, create_comparison_chart, create_safety_gauge, ) from src.data.solvers.beam import BEAM_SOLVERS from src.data.solvers.plate import PLATE_SOLVERS from src.data.solvers.vessel import VESSEL_SOLVERS # ═══════════════════════════════════════════════════════════════════ # Configuration # ═══════════════════════════════════════════════════════════════════ PROBLEM_TYPES = { "Simply Supported Beam — Point Load": "beam_ss_point", "Simply Supported Beam — UDL": "beam_ss_udl", "Cantilever Beam — Point Load": "beam_cantilever_point", "Cantilever Beam — UDL": "beam_cantilever_udl", "Fixed-Fixed Beam — Point Load": "beam_fixed_point", "Fixed-Fixed Beam — UDL": "beam_fixed_udl", "Simply Supported Plate — Uniform Pressure": "plate_ss_uniform", "Clamped Plate — Uniform Pressure": "plate_fixed_uniform", "Thick-Walled Cylinder": "vessel_cylinder", "Thick-Walled Sphere": "vessel_sphere", } ALL_SOLVERS = {**BEAM_SOLVERS, **PLATE_SOLVERS, **VESSEL_SOLVERS} # ═══════════════════════════════════════════════════════════════════ # Core prediction function # ═══════════════════════════════════════════════════════════════════ def predict( problem_type: str, length: float, width: float, height: float, inner_radius: float, outer_radius: float, thickness: float, material_name: str, elastic_modulus_gpa: float, poisson_ratio: float, yield_strength_mpa: float, density: float, load_value: float, pressure_value: float, ): """Run the analytical solver and return formatted results + plots.""" # Convert display units back to SI elastic_modulus = elastic_modulus_gpa * 1e9 yield_strength = yield_strength_mpa * 1e6 config_id = PROBLEM_TYPES.get(problem_type, "beam_ss_point") family = config_id.split("_")[0] # Build solver params based on problem family if family == "beam": load_key = "point_load" if "point" in config_id else "distributed_load" solver_params: dict[str, Any] = { "length": length, "width": width, "height": height, "elastic_modulus": elastic_modulus, "yield_strength": yield_strength, load_key: load_value, } elif family == "plate": solver_params = { "length_a": length, "length_b": width, "thickness": thickness, "elastic_modulus": elastic_modulus, "poisson_ratio": poisson_ratio, "yield_strength": yield_strength, "pressure": pressure_value, } else: # vessel solver_params = { "inner_radius": inner_radius, "outer_radius": outer_radius, "elastic_modulus": elastic_modulus, "poisson_ratio": poisson_ratio, "yield_strength": yield_strength, "internal_pressure": pressure_value, } # Run analytical solver start_time = time.perf_counter() try: solver = ALL_SOLVERS[config_id]() analytical = solver.solve(solver_params) except Exception as exc: error_md = f"### Error\n\nSolver failed: `{exc}`\n\nCheck input parameters and try again." return error_md, None, None latency_ms = (time.perf_counter() - start_time) * 1000 # Compute safety classification sf = analytical.safety_factor if sf >= 2.0: safety_badge = "SAFE" safety_color = "#66BB6A" elif sf >= 1.0: safety_badge = "MARGINAL" safety_color = "#FFA726" else: safety_badge = "FAILURE" safety_color = "#EF5350" # Results markdown results_md = f""" ### Analytical Solution | Metric | Value | |--------|-------| | **Max Stress** | {analytical.max_stress / 1e6:.3f} MPa | | **Max Deflection** | {analytical.max_deflection * 1e3:.4f} mm | | **Safety Factor** | {analytical.safety_factor:.3f} | | **Yield Strength** | {yield_strength / 1e6:.1f} MPa | **Status:** {safety_badge}   |   **Computed in {latency_ms:.2f} ms** --- *Using closed-form {family} solution with {material_name}. See the "Model Info" tab for the full theory reference.* """ # Generate plots (use analytical stress as the "neural" value so the # comparison chart shows equal bars; keeps the existing figure code # working without modifications) comparison_fig = create_comparison_chart( neural_stress=analytical.max_stress, analytical_stress=analytical.max_stress, neural_deflection=analytical.max_deflection, analytical_deflection=analytical.max_deflection, stress_ci=(analytical.max_stress * 0.999, analytical.max_stress * 1.001), deflection_ci=(analytical.max_deflection * 0.999, analytical.max_deflection * 1.001), ) if family == "beam": deform_fig = create_beam_deformation( length, height, analytical.max_deflection, config_id ) else: deform_fig = create_safety_gauge(sf) return results_md, comparison_fig, deform_fig def update_material(material_name: str): """Update material property fields when preset is selected.""" props = MATERIAL_PRESETS.get(material_name, MATERIAL_PRESETS.get("ASTM A36 Steel")) return ( props["elastic_modulus"] / 1e9, props["poisson_ratio"], props["yield_strength"] / 1e6, props["density"], ) def update_visibility(problem_type: str): """Show/hide input fields based on problem type.""" config_id = PROBLEM_TYPES.get(problem_type, "beam_ss_point") is_beam = config_id.startswith("beam") is_plate = config_id.startswith("plate") is_vessel = config_id.startswith("vessel") is_point = "point" in config_id return ( gr.Number(visible=is_beam or is_plate), # length gr.Number(visible=is_beam or is_plate), # width gr.Number(visible=is_beam), # height gr.Number(visible=is_vessel), # inner_radius gr.Number(visible=is_vessel), # outer_radius gr.Number(visible=is_plate), # thickness gr.Number( visible=is_beam, label="Point Load [N]" if is_point else "Distributed Load [N/m]", ), gr.Number(visible=is_plate or is_vessel), # pressure ) # ═══════════════════════════════════════════════════════════════════ # Gradio UI # ═══════════════════════════════════════════════════════════════════ with gr.Blocks(title="FEA Surrogate — Structural Analysis", theme=gr.themes.Soft()) as demo: gr.Markdown( """ # FEA Surrogate — Structural Analysis Engine **Physics-informed neural surrogate** for fast structural analysis with uncertainty quantification. This demo runs the **analytical solvers** (Euler-Bernoulli, Kirchhoff, Lame) that generate the training data for the neural model. > **Neural mode coming soon** — once NYU Torch supercomputer access > is activated, the trained PI-ResMLP deep ensemble will replace > the analytical solver with a ~1000× faster ML prediction that > includes calibrated uncertainty intervals. """ ) with gr.Tabs(): # ───────────────────────────────────────────────────────── # Tab 1 — Predict # ───────────────────────────────────────────────────────── with gr.Tab("Predict"): with gr.Row(): with gr.Column(scale=4): problem_type = gr.Dropdown( choices=list(PROBLEM_TYPES.keys()), value="Simply Supported Beam — Point Load", label="Problem Type", ) gr.Markdown("#### Geometry") with gr.Row(): length_input = gr.Number(value=2.0, label="Length [m]", minimum=0.01) width_input = gr.Number(value=0.05, label="Width [m]", minimum=0.001) height_input = gr.Number(value=0.10, label="Height [m]", minimum=0.001) with gr.Row(): inner_r = gr.Number(value=0.1, label="Inner Radius [m]", visible=False) outer_r = gr.Number(value=0.15, label="Outer Radius [m]", visible=False) thick = gr.Number(value=0.01, label="Thickness [m]", visible=False) gr.Markdown("#### Material") material_dropdown = gr.Dropdown( choices=MATERIAL_NAMES, value="ASTM A36 Steel", label="Material Preset", ) with gr.Row(): e_mod = gr.Number(value=200.0, label="E [GPa]") nu = gr.Number(value=0.26, label="Poisson's Ratio") with gr.Row(): sig_y = gr.Number(value=250.0, label="Yield Strength [MPa]") dens = gr.Number(value=7850.0, label="Density [kg/m³]") gr.Markdown("#### Loads") with gr.Row(): load_value = gr.Number(value=10000.0, label="Point Load [N]") pressure_value = gr.Number(value=100000.0, label="Pressure [Pa]", visible=False) predict_btn = gr.Button("Run Analysis", variant="primary", size="lg") with gr.Column(scale=6): results_output = gr.Markdown() comparison_plot = gr.Plot(label="Stress & Deflection") deform_plot = gr.Plot(label="Deformation / Safety Gauge") # Wiring material_dropdown.change( update_material, inputs=[material_dropdown], outputs=[e_mod, nu, sig_y, dens], ) problem_type.change( update_visibility, inputs=[problem_type], outputs=[ length_input, width_input, height_input, inner_r, outer_r, thick, load_value, pressure_value, ], ) predict_btn.click( predict, inputs=[ problem_type, length_input, width_input, height_input, inner_r, outer_r, thick, material_dropdown, e_mod, nu, sig_y, dens, load_value, pressure_value, ], outputs=[results_output, comparison_plot, deform_plot], ) # ───────────────────────────────────────────────────────── # Tab 2 — Theory # ───────────────────────────────────────────────────────── with gr.Tab("Theory"): gr.Markdown( """ ## Analytical Foundations The ground-truth dataset for the neural surrogate is generated from closed-form solutions of classical elasticity theory. These are the same equations used throughout undergraduate mechanical engineering. ### Beam Theory (Euler-Bernoulli) For a beam with elastic modulus $E$, moment of inertia $I$, and length $L$: **Simply supported, central point load $P$:** - Max deflection: $\\delta_{max} = \\frac{PL^3}{48EI}$ - Max stress: $\\sigma_{max} = \\frac{PL}{4S}$ where $S$ is section modulus **Cantilever, tip point load $P$:** - Max deflection: $\\delta_{max} = \\frac{PL^3}{3EI}$ - Max stress: $\\sigma_{max} = \\frac{PL}{S}$ **Simply supported, uniform load $w$:** - Max deflection: $\\delta_{max} = \\frac{5wL^4}{384EI}$ - Max stress: $\\sigma_{max} = \\frac{wL^2}{8S}$ Reference: Timoshenko & Gere, *Mechanics of Materials* ### Plate Theory (Kirchhoff) Thin plates under uniform pressure $q$: **Simply supported rectangular plate:** - Max deflection: $w_{max} = \\alpha \\frac{q a^4}{Eh^3}$ - Max stress: $\\sigma_{max} = \\beta \\frac{q a^2}{h^2}$ Where $\\alpha$ and $\\beta$ depend on the aspect ratio $a/b$ and are tabulated in Timoshenko's *Theory of Plates and Shells*. ### Thick-Walled Vessels (Lame) For a cylinder with internal pressure $p_i$, inner radius $r_i$, outer radius $r_o$: - Hoop stress: $\\sigma_{\\theta} = \\frac{p_i r_i^2}{r_o^2 - r_i^2}\\left(1 + \\frac{r_o^2}{r^2}\\right)$ - Radial stress: $\\sigma_r = \\frac{p_i r_i^2}{r_o^2 - r_i^2}\\left(1 - \\frac{r_o^2}{r^2}\\right)$ Max stress occurs at the inner wall ($r = r_i$). ### Von Mises Failure Criterion For a multi-axial stress state: $$\\sigma_{VM} = \\sqrt{\\frac{(\\sigma_1 - \\sigma_2)^2 + (\\sigma_2 - \\sigma_3)^2 + (\\sigma_3 - \\sigma_1)^2}{2}}$$ Yielding occurs when $\\sigma_{VM} \\geq \\sigma_Y$. """ ) # ───────────────────────────────────────────────────────── # Tab 3 — Model Info # ───────────────────────────────────────────────────────── with gr.Tab("Model Info"): gr.Markdown( """ ## Neural Surrogate Architecture (coming soon) ### Current: Analytical Mode This HF Space runs the **exact analytical solvers** that form the training dataset. They are fast, exact, and require no ML — ideal for validating the pipeline end-to-end before neural training. ### Planned: PI-ResMLP Deep Ensemble Once **NYU Torch** supercomputer access is activated, the Space will upgrade to a trained neural surrogate with: - **Architecture:** Physics-Informed Residual MLP (PI-ResMLP) with skip connections and physics loss - **Ensemble:** 5 independently trained members for law-of-total-variance uncertainty quantification - **Dataset:** 100K analytically-generated samples (Latin Hypercube Sampling) - **Training:** Mixed precision (fp16), early stopping, gradient clipping, cosine learning rate schedule - **Evaluation:** R², MAPE, calibration curves, physics constraint satisfaction ### Why Neural Surrogate? Analytical solvers are exact but limited to the idealized geometries they were derived for. A neural surrogate trained on the analytical solutions can: 1. **Generalize** to combinations not in the training set 2. **Provide uncertainty** estimates via ensemble variance 3. **Run 1000× faster** than traditional FEA for complex geometries (after training is done) 4. **Embed physics** via the loss function, improving extrapolation beyond the training distribution ### Tech Stack - PyTorch + torch.compile - Mixed precision training (fp16 autocast) - Latin Hypercube Sampling for dataset generation - Log-transform + standardization for numerical stability - Deep ensemble uncertainty quantification - Gradio for the UI - Hugging Face Spaces for deployment - NYU Torch supercomputer for training """ ) gr.Markdown( """ --- **Source:** [github.com/wolfwdavid/ai-tools-collection](https://github.com/wolfwdavid/ai-tools-collection)  |  **HF Profile:** [@WolfDavid](https://huggingface.co/WolfDavid) """ ) if __name__ == "__main__": demo.launch()