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)