|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
import math |
|
|
import json |
|
|
import gradio as gr |
|
|
import pandas as pd |
|
|
|
|
|
|
|
|
_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.") |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
|
|
sigma_ax = N / A |
|
|
sigma_bx = (Mx * c) / I if I > 0 else math.inf |
|
|
sigma_by = (My * c) / I if I > 0 else math.inf |
|
|
|
|
|
|
|
|
sigma_plus = sigma_ax + abs(sigma_bx) + abs(sigma_by) |
|
|
sigma_minus = sigma_ax - abs(sigma_bx) - abs(sigma_by) |
|
|
|
|
|
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)" |
|
|
|
|
|
|
|
|
tau = (T * c) / J if J > 0 else math.inf |
|
|
|
|
|
|
|
|
sigma_vm = (sigma**2 + 3.0 * tau**2) ** 0.5 |
|
|
Sy_Pa = Sy_MPa * 1e6 |
|
|
fos = Sy_Pa / sigma_vm if sigma_vm > 0 else math.inf |
|
|
ok = sigma_vm <= Sy_Pa |
|
|
|
|
|
|
|
|
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) |
|
|
|