# ========================= # 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)