GUI3 / app.py
its-zion-18's picture
Update app.py
027bf26 verified
import math # Used for mathematical functions like radians and pi
import gradio as gr # Framework for building the web interface
import pandas as pd # Used for creating the structured output table (DataFrame)
from transformers import AutoTokenizer, AutoModelForCausalLM, pipeline # Core components for running the HuggingFace LLM
# Specifies the smaller, instructional model for low-latency recommendations
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,
device=-1 # Runs on CPU by default; set device=0 for GPU
)
# 1. Calculation Function
def gear_calc(N_teeth: int, Pd: float, phi_deg: float) -> dict:
"""
Calculates standard dimensions for an external spur gear based on common engineering formulas.
Inputs:
- N_teeth (int): Number of teeth
- Pd (float): Diametral Pitch [teeth/inch]
- phi_deg (float): Pressure Angle in degrees
Returns:
- dict: A dictionary containing all standard gear dimensions in both mm and inches.
"""
if N_teeth <= 0 or Pd <= 0 or phi_deg <= 0:
# Input validation: all gear parameters must be positive
raise ValueError("All inputs must be positive.")
phi_rad = math.radians(phi_deg)
# Pitch Diameter (D) - The imaginary circle upon which the tooth spacing is measured.
pitch_diameter_in = N_teeth / Pd
pitch_diameter_mm = pitch_diameter_in * 25.4
# Addendum (a) and Dedendum (b) - Heights above and depths below the pitch circle.
addendum_in = 1 / Pd
dedendum_in = 1.25 / Pd
addendum_mm = addendum_in * 25.4
dedendum_mm = dedendum_in * 25.4
# Outside (OD) and Root Diameters - The largest and smallest diameters of the gear.
od_in = pitch_diameter_in + 2 * addendum_in
root_diameter_in = pitch_diameter_in - 2 * dedendum_in
od_mm = od_in * 25.4
root_diameter_mm = root_diameter_in * 25.4
# Working Depth and Whole Depth - Total contact depth and total tooth height.
working_depth_in = 2 * addendum_in
whole_depth_in = addendum_in + dedendum_in
working_depth_mm = working_depth_in * 25.4
whole_depth_mm = whole_depth_in * 25.4
# Circular Pitch (p) and Tooth Thickness (t) - Spacing and width of the tooth along the pitch circle.
circular_pitch_in = math.pi / Pd
circular_pitch_mm = circular_pitch_in * 25.4
tooth_thickness_in = circular_pitch_in / 2
tooth_thickness_mm = tooth_thickness_in * 25.4
# Base Diameter (Db) - The circle from which the involute curve is generated.
base_diameter_in = pitch_diameter_in * math.cos(phi_rad)
base_diameter_mm = base_diameter_in * 25.4
# Return a comprehensive dictionary of all calculated dimensions
return dict(
pitch_diameter_mm=pitch_diameter_mm,
pitch_diameter_in=pitch_diameter_in,
od_mm=od_mm,
od_in=od_in,
root_diameter_mm=root_diameter_mm,
root_diameter_in=root_diameter_in,
addendum_mm=addendum_mm,
addendum_in=addendum_in,
dedendum_mm=dedendum_mm,
dedendum_in=dedendum_in,
working_depth_mm=working_depth_mm,
working_depth_in=working_depth_in,
whole_depth_mm=whole_depth_mm,
whole_depth_in=whole_depth_in,
circular_pitch_mm=circular_pitch_mm,
circular_pitch_in=circular_pitch_in,
tooth_thickness_mm=tooth_thickness_mm,
tooth_thickness_in=tooth_thickness_in,
base_diameter_mm=base_diameter_mm,
base_diameter_in=base_diameter_in,
)
# 2. LLM Helper Functions
def _format_chat(system_prompt: str, user_prompt: str) -> str:
"""
Helper function to structure the system and user prompts into the LLM's required
chat template format for optimal instruction adherence.
"""
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
)
def _llm_generate(prompt: str, max_tokens: int) -> str:
"""
Executes the text generation pipeline using deterministic settings (do_sample=False,
temperature=0.0) to ensure the LLM provides the most compliant, non-creative output.
"""
out = pipe(
prompt,
max_new_tokens=max_tokens,
do_sample=False, # Disable random sampling for deterministic results
temperature=0.0, # Set temperature to 0.0 for maximum compliance
return_full_text=False,
)
# Strip leading/trailing whitespace from the generated text
return out[0]["generated_text"].strip()
# 3. LLM Explanation Function
def llm_explain(results: dict, inputs: list) -> str:
"""
Generates a clear, customized explanation for a non-technical user
based on the calculated gear dimensions.
"""
pitch_diameter_in = results['pitch_diameter_in']
tooth_thickness_in = results['tooth_thickness_in']
circular_pitch_in = results['circular_pitch_in']
# Estimate scale
if pitch_diameter_in < 2:
scale = "small"
examples = "clocks, cameras, or precision instruments"
elif pitch_diameter_in < 10:
scale = "medium"
examples = "bicycles, sewing machines, or automotive gearboxes"
else:
scale = "large"
examples = "wind turbines, cranes, or heavy mining machinery"
# Interpret tooth strength
if tooth_thickness_in < 0.05:
strength = "delicate and precise, best for very light loads"
elif tooth_thickness_in < 0.3:
strength = "balanced between precision and strength"
else:
strength = "very strong, made for heavy-duty work"
# Interpret circular pitch (spacing)
if circular_pitch_in < 0.2:
spacing = "fine and closely spaced for smooth, quiet motion"
elif circular_pitch_in < 1:
spacing = "moderate spacing for reliable general use"
else:
spacing = "wide spacing, rugged enough to handle shocks and dirt"
# System prompt
system_prompt = (
"You are a mechanical engineer explaining gears to a non-technical user. "
"Write in 2–4 friendly sentences. "
"Explain what the size suggests, what the tooth shape/spacing means in practice, "
"and give unique example applications that fit the gear’s scale. "
"Do not simply restate the numbers given. "
"Be specific and avoid repeating the same examples across different gear sizes."
)
# User prompt (gear context)
user_prompt = (
f"This is a {scale} gear. "
f"Its teeth are {strength}, and the spacing is {spacing}. "
f"Suggest everyday uses where a gear like this would make sense, such as {examples}. "
"Explain it in a way that feels useful and practical for someone who is not an engineer."
)
formatted = _format_chat(system_prompt, user_prompt)
explanation = _llm_generate(formatted, max_tokens=200)
# Fallback if the LLM fails
if not explanation:
explanation = (
f"This is a {scale} gear with {strength} and {spacing}. "
f"You’d expect to find gears like this in {examples}."
)
return explanation
# 4. Main Entry Point for GUI
def run_once(N_teeth_str, Pd_str, phi_deg_str):
"""
Main function executed by the Gradio interface. Handles input parsing,
calculation, result formatting (DataFrame), and LLM generation.
"""
try:
# Input conversion from string to required types
N_teeth = int(N_teeth_str)
Pd = float(Pd_str)
phi_deg = float(phi_deg_str)
inputs = [N_teeth, Pd, phi_deg]
# Perform the core mechanical calculation
results = gear_calc(N_teeth, Pd, phi_deg)
# Prepare the data structure for the Pandas DataFrame output table
rows = [
# Each dictionary represents a row in the output table
{"Quantity": "Pitch Diameter", "Value (mm)": results["pitch_diameter_mm"], "Value (in)": results["pitch_diameter_in"]},
{"Quantity": "Outside Diameter (OD)", "Value (mm)": results["od_mm"], "Value (in)": results["od_in"]},
{"Quantity": "Root Diameter", "Value (mm)": results["root_diameter_mm"], "Value (in)": results["root_diameter_in"]},
{"Quantity": "Base Diameter", "Value (mm)": results["base_diameter_mm"], "Value (in)": results["base_diameter_in"]},
{"Quantity": "Addendum", "Value (mm)": results["addendum_mm"], "Value (in)": results["addendum_in"]},
{"Quantity": "Dedendum", "Value (mm)": results["dedendum_mm"], "Value (in)": results["dedendum_in"]},
{"Quantity": "Working Depth", "Value (mm)": results["working_depth_mm"], "Value (in)": results["working_depth_in"]},
{"Quantity": "Whole Depth", "Value (mm)": results["whole_depth_mm"], "Value (in)": results["whole_depth_in"]},
{"Quantity": "Circular Pitch", "Value (mm)": results["circular_pitch_mm"], "Value (in)": results["circular_pitch_in"]},
{"Quantity": "Tooth Thickness", "Value (mm)": results["tooth_thickness_mm"], "Value (in)": results["tooth_thickness_in"]},
]
# Create and format the output DataFrame, rounding values for display
df = pd.DataFrame(rows)
df["Value (mm)"] = df["Value (mm)"].round(4)
df["Value (in)"] = df["Value (in)"].round(4)
# Generate the LLM narrative
narrative = llm_explain(results, inputs)
return df, narrative
except ValueError as e:
# Handle mathematical and input validation errors
error_df = pd.DataFrame([{"Error": str(e)}])
return error_df, "Error: Please ensure all inputs are valid positive numbers. Teeth must be an integer."
except Exception as e:
# Handle unexpected system/LLM errors
error_df = pd.DataFrame([{"System Error": "Calculation or LLM failed"}])
print(f"Internal Error: {e}")
return error_df, "An unexpected system error occurred during computation or LLM generation."
# Gradio UI Definition
with gr.Blocks() as demo:
# App Title and Description
gr.Markdown(
"# External Gear Dimension Calculator ⚙️"
)
gr.Markdown(
"Enter number of teeth, diametral pitch, and pressure angle to get standard external gear dimensions, and an LLM-generated use case."
)
# Input parameters organized in a row layout
with gr.Row():
N_teeth_in = gr.Number(value=0, label="Number of Teeth (#)", precision=0)
Pd_in = gr.Number(value=0, label="Diametral Pitch [teeth/inch]")
phi_deg_in = gr.Number(value=0, label="Pressure Angle [°]")
# Action button to trigger the process
run_btn = gr.Button("Calculate and Explain Gear Use")
# Output components: DataFrame for numbers, Markdown for LLM text
results_df = gr.Dataframe(label="Gear Dimensions (mm and inches)", interactive=False)
explain_md = gr.Markdown(label="LLM-Generated Use Case")
# Event listener: Connect the button click to the main computation function
run_btn.click(fn=run_once, inputs=[N_teeth_in, Pd_in, phi_deg_in], outputs=[results_df, explain_md])
# Pre-defined examples for easy testing
gr.Examples(
examples=[
[12, 4.0, 20.0],
[40, 0.75, 45.0],
[200, 0.5, 25.0],
],
inputs=[N_teeth_in, Pd_in, phi_deg_in],
label="Representative Gear Cases",
examples_per_page=3,
cache_examples=False,
)
if __name__ == "__main__":
demo.launch(debug=True)