fea-surrogate / app.py
WolfDavid's picture
HF Spaces deploy: analytical mode, no torch, pinned gradio 5.9.1
fcc3283
"""
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:** <span style="color:{safety_color};font-weight:bold;font-size:1.2em">{safety_badge}</span>
&nbsp;&nbsp;|&nbsp;&nbsp;
**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)
&nbsp;|&nbsp;
**HF Profile:** [@WolfDavid](https://huggingface.co/WolfDavid)
"""
)
if __name__ == "__main__":
demo.launch()