File size: 11,813 Bytes
7c497b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
027bf26
 
7c497b8
027bf26
7c497b8
027bf26
 
7c497b8
027bf26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7c497b8
027bf26
 
 
 
 
 
7c497b8
 
027bf26
7c497b8
027bf26
 
 
 
7c497b8
 
 
027bf26
 
 
 
7c497b8
027bf26
 
 
 
 
7c497b8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
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)