SebastianAndreu's picture
Create app.py
6b36e09 verified
# ================================================================
# HW3: Bolt Torque Calculator with Gradio
#
# Author: Sebastian Andreu
# Course: 24679 - Designing and Deploying AI/ML Systems
# Dataset/Inputs: User-selected bolt geometry, preload, material, lubrication
# Task: Deterministic first-principles calculation of bolt torque with natural language explanation,
# deployed via Hugging Face Space
#
# Acknowledgments:
# - Torque calculation formulas based on standard ISO metric bolt theory
# - Deployment scaffold and documentation supported with AI assistance (ChatGPT, OpenAI)
# - Reference: Class-provided notebook "LLMs for explanability.ipynb"
# ================================================================
import math # For trigonometry
import gradio # For building the interface
import pandas # For working with tables
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline # For LLMs
# Instantiate the model that we'll be calling. This is a tiny one!
MODEL_ID = "HuggingFaceTB/SmolLM2-135M-Instruct"
tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
pipe = pipeline(
task="text-generation",
model=AutoModelForCausalLM.from_pretrained(
MODEL_ID,
),
tokenizer=tokenizer
)
# Lookup table for friction coefficients
# Format: friction_table[material][lubrication] = coefficient
friction_table = {
"Steel": {"Dry": 0.15, "Oil": 0.10, "Grease": 0.08, "Zinc/Anti-seize": 0.05},
"Aluminum": {"Dry": 0.20, "Oil": 0.15, "Grease": 0.12, "Zinc/Anti-seize": 0.10},
"Brass": {"Dry": 0.18, "Oil": 0.13, "Grease": 0.10, "Zinc/Anti-seize": 0.08},
"Titanium": {"Dry": 0.25, "Oil": 0.20, "Grease": 0.18, "Zinc/Anti-seize": 0.15},
}
# Function to calculate bolt torque
def bolt_calc(
d_mm: float,
F_N: float,
p_mm: float,
mu_t: float,
mu_n: float,
d_head_mm: float = None
) -> dict:
"""
Calculates required torque for a standard ISO metric bolt.
Returns torque [Nm] and a message.
"""
# Thread half-angle (ISO metric)
beta_rad = math.radians(30)
# Lead angle
phi_rad = math.atan(p_mm / (math.pi * d_mm))
# If head diameter not provided, assume 1.5 * bolt diameter
if d_head_mm is None:
d_head_mm = 1.5 * d_mm
# Friction angle
rho_rad = math.atan(mu_t / math.cos(beta_rad))
# Torque calculation
T_Nm = F_N * (d_mm / 2 * math.tan(phi_rad + rho_rad) + mu_n * d_head_mm / 2)
return dict(
results={
"torque_Nm": T_Nm,
},
verdict={
"strength_message": "Torque calculated successfully",
},
)
# Helper for chat formatting
def _format_chat(system_prompt: str, user_prompt: str) -> str:
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_prompt},
]
template = getattr(tokenizer, "chat_template", None)
return tokenizer.apply_chat_template(
messages,
tokenize=False,
add_generation_prompt=True
)
# LLM text generation
def _llm_generate(prompt: str, max_tokens: int) -> str:
out = pipe(
prompt,
max_new_tokens=max_tokens,
do_sample=True,
temperature=0.5,
return_full_text=False,
)
return out[0]["generated_text"]
# Generate natural language explanation
def llm_explain(results: dict, inputs: list) -> str:
d_mm, F_N, p_mm, mu_t, mu_n, d_head_mm = inputs
r = results["results"]
v = results["verdict"]
system_prompt = (
"You explain engineering to a smart 5-year-old. "
"Use simple analogies like screwing a jar lid or tightening a bike seat. "
"You always return CONCISE responses, only one sentence."
)
user_prompt = (
f"For a bolt of diameter {d_mm:g} mm with a target preload of {F_N:g} N, "
f"thread pitch {p_mm:g} mm, thread friction {mu_t:g}, "
f"nut/under-head friction {mu_n:g}, and head diameter {d_head_mm:g} mm:\n"
f"The required torque is {r['torque_Nm']:.2f} Nm; "
"Explain this torque in ONE friendly sentence for a non-expert."
)
formatted = _format_chat(system_prompt, user_prompt)
return _llm_generate(formatted, max_tokens=128)
# Run everything together
def run_once(d_mm, F_N, p_mm, thread_material, thread_lubrication, head_material, head_lubrication, d_head_mm):
# Map dropdown selections to friction coefficients
mu_t = friction_table[thread_material][thread_lubrication]
mu_n = friction_table[head_material][head_lubrication]
if d_head_mm is None:
d_head_mm = float(d_mm) * 1.5
inputs = [float(d_mm), float(F_N), float(p_mm), mu_t, mu_n, float(d_head_mm)]
d = bolt_calc(
d_mm=float(d_mm),
F_N=float(F_N),
p_mm=float(p_mm),
mu_t=mu_t,
mu_n=mu_n,
d_head_mm=float(d_head_mm)
)
df = pandas.DataFrame([{
"Torque [Nm]": round(d["results"]["torque_Nm"], 3),
"Verdict": d["verdict"]["strength_message"],
}])
narrative = llm_explain(d, inputs).split("\n")[0]
return df, narrative
# Build the Gradio interface
with gradio.Blocks() as demo:
gradio.Markdown(
"# Bolt Torque Calculator"
)
gradio.Markdown(
"Compute the torque needed to tighten a bolt to a target preload with material and lubrication selection."
)
# Bolt geometry and load
with gradio.Row():
d_mm = gradio.Number(value=10.0, label="Bolt diameter [mm]")
F_N = gradio.Number(value=5000.0, label="Target preload [N]")
p_mm = gradio.Number(value=1.5, label="Thread pitch [mm]")
# Thread friction selection
with gradio.Row():
thread_material = gradio.Dropdown(
choices=["Steel", "Aluminum", "Brass", "Titanium"],
value="Steel",
label="Bolt material"
)
thread_lubrication = gradio.Dropdown(
choices=["Dry", "Oil", "Grease", "Zinc/Anti-seize"],
value="Dry",
label="Thread lubrication"
)
head_material = gradio.Dropdown(
choices=["Steel", "Aluminum", "Brass", "Titanium"],
value="Steel",
label="Nut/Head material"
)
head_lubrication = gradio.Dropdown(
choices=["Dry", "Oil", "Grease", "Zinc/Anti-seize"],
value="Dry",
label="Nut/Head lubrication"
)
d_head_mm = gradio.Number(value=None, label="Head diameter [mm] (optional)")
run_btn = gradio.Button("Compute")
results_df = gradio.Dataframe(label="Numerical results (deterministic)", interactive=False)
explain_md = gradio.Markdown(label="Explanation")
run_btn.click(
fn=run_once,
inputs=[d_mm, F_N, p_mm, thread_material, thread_lubrication, head_material, head_lubrication, d_head_mm],
outputs=[results_df, explain_md]
)
gradio.Examples(
examples=[
[10.0, 5000.0, 1.5, "Steel", "Dry", "Steel", "Dry", None],
[12.0, 8000.0, 1.75, "Aluminum", "Oil", "Steel", "Grease", None],
[8.0, 2000.0, 1.25, "Titanium", "Grease", "Aluminum", "Oil", None],
],
inputs=[d_mm, F_N, p_mm, thread_material, thread_lubrication, head_material, head_lubrication, d_head_mm],
label="Representative cases",
examples_per_page=3,
cache_examples=False,
)
if __name__ == "__main__":
demo.launch(debug=True)