george2cool36's picture
Upload app.py with huggingface_hub
7b2f3fb verified
# =========================
# Combined Loading Calculator — Circular (Solid/Hollow)
# Axial N, Bending (Mx, My), Torsion T → σ, τ, σ_vm, FoS
# =========================
import math
import json
import gradio as gr
import pandas as pd
# Optional tiny LLM (safe fallback to deterministic message if not available)
_USE_LLM = True
try:
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline
MODEL_ID = "HuggingFaceTB/SmolLM2-135M-Instruct"
_tok = AutoTokenizer.from_pretrained(MODEL_ID)
_pipe = pipeline(
task="text-generation",
model=AutoModelForCausalLM.from_pretrained(MODEL_ID),
tokenizer=_tok,
)
except Exception:
_USE_LLM = False
_tok = None
_pipe = None
SCOPE_MD = r"""
### Scope & Assumptions
- **Cross-section:** Circular shaft or beam (choose **Solid** or **Hollow**).
- **Loads:** Axial force *N* (tension +), bending moments *Mₓ* and *Mᵧ*, and torsion *T*.
- **Outputs:** Axial stress at the outer surface (from axial + bending), shear stress from torsion, **von Mises** equivalent stress, and **FoS** vs. yield strength *Sᵧ*.
- **Theory:** Linear-elastic, small deformation, pure torsion (Saint-Venant), plane sections remain plane.
*Excludes* stress concentrations, transverse shear (**V**), and buckling.
- **Units:** SI — forces in **N**, moments in **N·m**, diameters in **m**, moduli/strengths in **GPa/MPa**.
Results are shown in **MPa**.
---
### Valid ranges (hard checks)
- 0.01 < dₒ ≤ 2.0 m
- 0 < dᵢ < dₒ (for hollow sections)
- |N| ≤ 1×10⁶ N (± tension/compression)
- |Mₓ|, |Mᵧ|, |T| ≤ 1×10⁶ N·m
- 1 ≤ E ≤ 400 GPa
- 10 ≤ Sᵧ ≤ 3000 MPa
- 0.00001 < A ≤ 1 m²
"""
def _validate(mode, d, do, di, N, Mx, My, T, Sy):
errs = []
def rng(name, val, lo, hi):
if not (lo < val <= hi):
errs.append(f"{name} must be in ({lo}, {hi}] (got {val}).")
rng("Sy [MPa]", Sy, 10, 3000)
if mode == "Solid":
rng("d [m]", d, 1e-4, 2.0)
else:
rng("Do [m]", do, 1e-4, 2.0)
rng("Di [m]", di, 0.0, do)
if di >= do:
errs.append("Hollow: require Do > Di.")
# moments/loads: allow 0 and ± large
for name, val in [("N [N]", N), ("Mx [N·m]", Mx), ("My [N·m]", My), ("T [N·m]", T)]:
if not math.isfinite(val):
errs.append(f"{name} must be finite.")
if errs:
raise ValueError("\n".join(errs))
def sect_props(mode, d, do, di):
if mode == "Solid":
A = math.pi * d**2 / 4.0
I = math.pi * d**4 / 64.0
J = math.pi * d**4 / 32.0
c = d / 2.0
outer_d = d
else:
A = math.pi * (do**2 - di**2) / 4.0
I = math.pi * (do**4 - di**4) / 64.0
J = math.pi * (do**4 - di**4) / 32.0
c = do / 2.0
outer_d = do
return A, I, J, c, outer_d
def combined_loading(mode, d, do, di, N, Mx, My, T, Sy_MPa):
_validate(mode, d, do, di, N, Mx, My, T, Sy_MPa)
A, I, J, c, outer_d = sect_props(mode, d, do, di)
# Normal stress at an outer point aligned with resultant bending
# Worst-case: take signs to maximize |sigma| → use absolute bending contributions added to axial sign
sigma_ax = N / A # Pa
sigma_bx = (Mx * c) / I if I > 0 else math.inf # Pa
sigma_by = (My * c) / I if I > 0 else math.inf # Pa
# Two extreme points (tension side / compression side). We'll report worst magnitude.
sigma_plus = sigma_ax + abs(sigma_bx) + abs(sigma_by)
sigma_minus = sigma_ax - abs(sigma_bx) - abs(sigma_by)
# Worst magnitude governs
if abs(sigma_plus) >= abs(sigma_minus):
sigma = sigma_plus
extreme_point = "+ (tension-side from bending)"
else:
sigma = sigma_minus
extreme_point = "− (compression-side from bending)"
# Shear at perimeter from torsion
tau = (T * c) / J if J > 0 else math.inf # Pa
# Von Mises
sigma_vm = (sigma**2 + 3.0 * tau**2) ** 0.5 # Pa
Sy_Pa = Sy_MPa * 1e6
fos = Sy_Pa / sigma_vm if sigma_vm > 0 else math.inf
ok = sigma_vm <= Sy_Pa
# Pretty helper
def _fmt(x, d=6):
try:
return f"{x:.{d}g}"
except Exception:
return str(x)
steps = []
steps.append("## Show the math (combined loading on circular section)")
if mode == "Solid":
steps.append(f"Mode: Solid | d = {_fmt(d)} m")
steps.append(f"A = π*d^2/4 = π*{d}^2/4 = {A:.6e} m^2")
steps.append(f"I = π*d^4/64 = π*{d}^4/64 = {I:.6e} m^4")
steps.append(f"J = π*d^4/32 = π*{d}^4/32 = {J:.6e} m^4")
steps.append(f"c = d/2 = {d}/2 = {c:.6e} m")
else:
steps.append(f"Mode: Hollow | Do = {_fmt(do)} m, Di = {_fmt(di)} m")
steps.append(f"A = π*(Do^2 - Di^2)/4 = π*({do}^2 - {di}^2)/4 = {A:.6e} m^2")
steps.append(f"I = π*(Do^4 - Di^4)/64 = π*({do}^4 - {di}^4)/64 = {I:.6e} m^4")
steps.append(f"J = π*(Do^4 - Di^4)/32 = π*({do}^4 - {di}^4)/32 = {J:.6e} m^4")
steps.append(f"c = Do/2 = {do}/2 = {c:.6e} m")
steps += [
"",
f"N = {_fmt(N)} N, Mx = {_fmt(Mx)} N·m, My = {_fmt(My)} N·m, T = {_fmt(T)} N·m, Sy = {_fmt(Sy_MPa)} MPa",
"",
"Normal stress at an outer fiber (worst-case point):",
"σ = N/A ± (Mx*c)/I ± (My*c)/I",
f"σ_ax = {N} / {A:.6e} = {sigma_ax/1e6:.6f} MPa",
f"(Mx*c)/I = ({Mx} * {c:.6e}) / ({I:.6e}) = {sigma_bx/1e6:.6f} MPa",
f"(My*c)/I = ({My} * {c:.6e}) / ({I:.6e}) = {sigma_by/1e6:.6f} MPa",
f"σ_max (reported) = {sigma/1e6:.6f} MPa at extreme point {extreme_point}",
"",
"Shear from torsion at perimeter:",
"τ = T*c/J",
f"τ = ({T} * {c:.6e}) / ({J:.6e}) = {tau/1e6:.6f} MPa",
"",
"Von Mises:",
"σ_vm = sqrt( σ^2 + 3*τ^2 )",
f"σ_vm = sqrt( ({sigma/1e6:.6f})^2 + 3*({tau/1e6:.6f})^2 ) = {sigma_vm/1e6:.6f} MPa",
f"FoS = Sy / σ_vm = {Sy_MPa} / {sigma_vm/1e6:.6f} = {fos:.3f}",
f"Verdict: {'OK (below yield)' if ok else 'NOT OK (yields by von Mises)'}"
]
steps_md = "\n".join(steps)
results = {
"A_m2": A, "I_m4": I, "J_m4": J, "c_m": c, "outer_d_m": outer_d,
"sigma_MPa": sigma/1e6, "tau_MPa": tau/1e6,
"sigma_vm_MPa": sigma_vm/1e6, "FoS_yield": fos, "ok": bool(ok),
"extreme_point": extreme_point
}
verdict = {
"message": "OK: von Mises below yield" if ok else "NOT OK: von Mises exceeds yield",
"extreme_point": extreme_point
}
structured = {
"problem": "Combined loading on circular section (N, Mx, My, T)",
"mode": mode,
"inputs": {"N_N": N, "Mx_Nm": Mx, "My_Nm": My, "T_Nm": T, "Sy_MPa": Sy_MPa,
"d_m": d, "Do_m": do, "Di_m": di},
"section": {"A_m2": A, "I_m4": I, "J_m4": J, "c_m": c, "outer_d_m": outer_d},
"results": results,
"verdict": verdict
}
return results, verdict, steps_md, json.dumps(structured, indent=2)
def _format_chat(system_prompt: str, user_prompt: str) -> str:
if _tok is None:
return system_prompt + "\n\n" + user_prompt
msgs = [{"role":"system","content":system_prompt},{"role":"user","content":user_prompt}]
return _tok.apply_chat_template(msgs, tokenize=False, add_generation_prompt=True)
def llm_explain(structured_message: str) -> str:
if (not _USE_LLM) or (_pipe is None) or (_tok is None):
try:
d = json.loads(structured_message)
ok = d["results"]["ok"]
vm = d["results"]["sigma_vm_MPa"]
fos = d["results"]["FoS_yield"]
return f"Quick take: von Mises = {vm:.2f} MPa (FoS={fos:.2f}) → {'OK' if ok else 'NOT OK'}."
except Exception:
return "Quick take: combined loading computed; see results."
system = "Explain to an engineering student in ONE friendly sentence; refer to the computed von Mises and FoS."
user = "Summarize whether the part yields under combined axial, bending, and torsion.\n\n" + structured_message
out = _pipe(_format_chat(system, user), max_new_tokens=80, do_sample=True, temperature=0.3, return_full_text=False)
return out[0]["generated_text"].split("\n")[0]
def run_once(mode, d, do, di, N, Mx, My, T, Sy_MPa):
try:
res, ver, steps, structured = combined_loading(
mode=mode,
d=float(d) if d is not None else 0.0,
do=float(do) if do is not None else 0.0,
di=float(di) if di is not None else 0.0,
N=float(N), Mx=float(Mx), My=float(My), T=float(T),
Sy_MPa=float(Sy_MPa)
)
df = pd.DataFrame([{
"σ (outer) [MPa]": round(res["sigma_MPa"], 3),
"τ (torsion) [MPa]": round(res["tau_MPa"], 3),
"σ_vm [MPa]": round(res["sigma_vm_MPa"], 3),
"FoS (yield)": round(res["FoS_yield"], 3),
"Extreme point": res["extreme_point"],
"Verdict": ver["message"],
}])
narrative = llm_explain(structured)
return df, narrative, steps, ""
except Exception as e:
return pd.DataFrame(), "", "", f"Input error:\n{e}"
with gr.Blocks(title="Combined Loading — Circular") as demo:
gr.Markdown("# Combined Loading Calculator — Circular (Solid/Hollow)")
gr.Markdown(SCOPE_MD)
with gr.Row():
with gr.Column():
mode = gr.Radio(["Solid", "Hollow"], value="Solid", label="Section type")
d = gr.Number(value=0.05, label="d [m] (solid diameter)")
do = gr.Number(value=0.06, label="Do [m] (outer diameter)", visible=False)
di = gr.Number(value=0.03, label="Di [m] (inner diameter)", visible=False)
def _toggle(m):
if m == "Solid":
return [gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)]
else:
return [gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)]
mode.change(_toggle, inputs=[mode], outputs=[d, do, di])
with gr.Column():
gr.Markdown("### Loads & Material")
N = gr.Number(value=5e4, label="Axial N [N] (tension +)")
Mx = gr.Number(value=200.0, label="Mx [N·m]")
My = gr.Number(value=0.0, label="My [N·m]")
T = gr.Number(value=500.0, label="T [N·m]")
Sy = gr.Number(value=250.0, label="Yield strength Sy [MPa]")
run_btn = gr.Button("Compute")
gr.Markdown("### Results")
results_df = gr.Dataframe(label="Numerical results", interactive=False)
gr.Markdown("### Explain the result")
explain_md = gr.Markdown()
gr.Markdown("### Show the math")
steps_md = gr.Markdown()
err_box = gr.Textbox(label="Errors", interactive=False)
run_btn.click(
fn=run_once,
inputs=[mode, d, do, di, N, Mx, My, T, Sy],
outputs=[results_df, explain_md, steps_md, err_box]
)
if __name__ == "__main__":
demo.launch(debug=False)