scottymcgee commited on
Commit
5ebc3d3
·
verified ·
1 Parent(s): 6a737a1

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +260 -0
app.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- deps (minimal) ---
2
+ # !pip -q install "gradio>=4.44.0" "transformers>=4.42.0"
3
+
4
+ # --- deterministic backend ---
5
+ from math import pi
6
+ import re
7
+ import gradio as gr
8
+ from transformers import pipeline
9
+
10
+ def validate_and_compute(
11
+ driver_teeth:int,
12
+ driven_teeth:int,
13
+ input_torque_Nm:float,
14
+ input_speed_rpm:float,
15
+ efficiency_pct:float,
16
+ ):
17
+ """
18
+ Deterministic, first-principles 2-gear calculator.
19
+
20
+ SCOPE & ASSUMPTIONS
21
+ - Two rigid spur gears (driver -> driven), single mesh, no slippage.
22
+ - Quasi-steady operation; efficiency is mesh efficiency only.
23
+ - No shaft/bearing losses; no strength checks.
24
+
25
+ INPUTS & VALID RANGES
26
+ - driver_teeth: 6..400 (int)
27
+ - driven_teeth: 6..400 (int)
28
+ - input_torque_Nm: 0..10_000 [N·m]
29
+ - input_speed_rpm: 0..30_000 [rpm]
30
+ - efficiency_pct: 50..100 [%]
31
+ """
32
+ problems = []
33
+ if not isinstance(driver_teeth, int) or not (6 <= driver_teeth <= 400):
34
+ problems.append("driver_teeth must be an integer in [6, 400].")
35
+ if not isinstance(driven_teeth, int) or not (6 <= driven_teeth <= 400):
36
+ problems.append("driven_teeth must be an integer in [6, 400].")
37
+ if not (0 <= input_torque_Nm <= 10_000):
38
+ problems.append("input_torque_Nm must be in [0, 10_000] N·m.")
39
+ if not (0 <= input_speed_rpm <= 30_000):
40
+ problems.append("input_speed_rpm must be in [0, 30_000] rpm.")
41
+ if not (50 <= efficiency_pct <= 100):
42
+ problems.append("efficiency_pct must be in [50, 100] %.")
43
+
44
+ if problems:
45
+ return {"ok": False, "errors": problems}
46
+
47
+ gear_ratio = driven_teeth / driver_teeth # >1 → torque↑ speed↓
48
+ eta = efficiency_pct / 100.0
49
+
50
+ # kinematics
51
+ output_speed_rpm = input_speed_rpm / gear_ratio
52
+
53
+ # torque (power*eta conserved across mesh)
54
+ output_torque_Nm = input_torque_Nm * gear_ratio * eta
55
+
56
+ # power check
57
+ omega_in = input_speed_rpm * (2*pi/60.0) # rad/s
58
+ omega_out = output_speed_rpm * (2*pi/60.0) # rad/s
59
+ pin_W = input_torque_Nm * omega_in
60
+ pout_W = output_torque_Nm * omega_out
61
+ expected_pout_W = pin_W * eta
62
+ power_balance_error_pct = (0 if expected_pout_W == 0 else
63
+ 100.0 * (pout_W - expected_pout_W) / max(1e-12, expected_pout_W))
64
+
65
+ speed_change = "decrease" if gear_ratio > 1 else ("increase" if gear_ratio < 1 else "no change")
66
+ torque_change = "increase" if gear_ratio > 1 else ("decrease" if gear_ratio < 1 else "no change")
67
+
68
+ return {
69
+ "ok": True,
70
+ "inputs": {
71
+ "driver_teeth": driver_teeth,
72
+ "driven_teeth": driven_teeth,
73
+ "input_torque_Nm": float(input_torque_Nm),
74
+ "input_speed_rpm": float(input_speed_rpm),
75
+ "efficiency_pct": float(efficiency_pct)
76
+ },
77
+ "intermediate": {
78
+ "gear_ratio": float(gear_ratio),
79
+ "eta": float(eta)
80
+ },
81
+ "outputs": {
82
+ "output_speed_rpm": float(output_speed_rpm),
83
+ "output_torque_Nm": float(output_torque_Nm),
84
+ "input_power_W": float(pin_W),
85
+ "output_power_W": float(pout_W),
86
+ "expected_output_power_W": float(expected_pout_W),
87
+ "power_balance_error_percent": float(power_balance_error_pct),
88
+ "qualitative": {
89
+ "speed_change": speed_change,
90
+ "torque_change": torque_change
91
+ }
92
+ }
93
+ }
94
+
95
+ # --- compact, deterministic LLM explainer ---
96
+ explainer = pipeline("text2text-generation", model="google/flan-t5-small")
97
+
98
+ def sanitize_to_plain_sentences(text: str, max_sentences: int = 6) -> str:
99
+ """Drop code/JSON-y lines and keep up to N plain sentences."""
100
+ # Strip code blocks/backticks
101
+ text = re.sub(r"```.*?```", "", text, flags=re.S)
102
+ text = text.replace("`", "")
103
+ # Keep only part after the last 'Explanation:' if present
104
+ if "Explanation:" in text:
105
+ text = text.split("Explanation:")[-1]
106
+ # Remove obvious code-ish lines
107
+ cleaned_lines = []
108
+ for line in text.splitlines():
109
+ l = line.strip()
110
+ if not l:
111
+ continue
112
+ if re.search(r"\binput\(", l) or re.search(r"\bprint\(", l):
113
+ continue
114
+ if re.match(r"^\s*[A-Za-z_]\w*\s*=", l): # assignments
115
+ continue
116
+ if l.startswith("{") or l.startswith("[") or l.endswith("}"):
117
+ continue
118
+ cleaned_lines.append(l)
119
+ text = " ".join(cleaned_lines)
120
+
121
+ # Split into sentences conservatively
122
+ # (avoid splitting inside units like N·m, rpm—these don’t have periods)
123
+ sentences = re.split(r"(?<=[.!?])\s+", text)
124
+ # Trim and keep sentences that look like natural language
125
+ plain = []
126
+ for s in sentences:
127
+ s = s.strip()
128
+ if not s:
129
+ continue
130
+ # Heuristics to avoid code/fragments
131
+ if re.search(r"[{};]|==|!=|<=|>=|\breturn\b|\bdef\b|\bclass\b", s):
132
+ continue
133
+ plain.append(s)
134
+ if len(plain) >= max_sentences:
135
+ break
136
+ return " ".join(plain).strip()
137
+
138
+ def fallback_explanation(payload: dict) -> str:
139
+ """Deterministic template if the LLM output gets sanitized to nothing/too short."""
140
+ inp = payload["inputs"]; mid = payload["intermediate"]; out = payload["outputs"]
141
+ sc = out["qualitative"]["speed_change"]; tc = out["qualitative"]["torque_change"]
142
+ return (
143
+ f"The driven gear has {inp['driven_teeth']} teeth and the driver has {inp['driver_teeth']}, "
144
+ f"so the gear ratio is {mid['gear_ratio']:.3f} (driven/driver). "
145
+ f"With an input torque of {inp['input_torque_Nm']:.3g} N·m at {inp['input_speed_rpm']:.3g} rpm, "
146
+ f"the output speed becomes {out['output_speed_rpm']:.3g} rpm (a {sc} in speed) and the output torque is "
147
+ f"{out['output_torque_Nm']:.3g} N·m (a {tc} in torque). "
148
+ f"Input power is {out['input_power_W']:.3g} W; with an efficiency of {inp['efficiency_pct']:.3g}%, "
149
+ f"the expected output power is {out['expected_output_power_W']:.3g} W, which matches the computed "
150
+ f"{out['output_power_W']:.3g} W. "
151
+ f"This calculation considers mesh efficiency only; check tooth strength, shafts, and bearings separately."
152
+ )
153
+
154
+ def render_explanation(payload: dict) -> str:
155
+ if not payload.get("ok"):
156
+ return "There were validation errors; please fix inputs and try again."
157
+
158
+ inp = payload["inputs"]; mid = payload["intermediate"]; out = payload["outputs"]
159
+
160
+ # Only pass essentials so the model can’t riff on raw JSON.
161
+ facts = (
162
+ f"Driver teeth: {inp['driver_teeth']}; Driven teeth: {inp['driven_teeth']}. "
163
+ f"Input torque: {inp['input_torque_Nm']:.4g} N·m; Input speed: {inp['input_speed_rpm']:.4g} rpm. "
164
+ f"Efficiency: {inp['efficiency_pct']:.4g}%. "
165
+ f"Gear ratio (driven/driver): {mid['gear_ratio']:.6g}. "
166
+ f"Output speed: {out['output_speed_rpm']:.6g} rpm; Output torque: {out['output_torque_Nm']:.6g} N·m. "
167
+ f"Input power: {out['input_power_W']:.6g} W; Output power: {out['output_power_W']:.6g} W; "
168
+ f"Expected Pin×η: {out['expected_output_power_W']:.6g} W."
169
+ )
170
+
171
+ prompt = (
172
+ "You are a careful engineering assistant. Using only the facts below, write 4–6 clear sentences that "
173
+ "explain the gear calculation to a non-expert. Mention the gear ratio’s tradeoff (speed vs torque) and "
174
+ "comment that output power is approximately input power times efficiency. "
175
+ "Do NOT include code, equations, lists, JSON, or symbols like '=', '{', '}'. Plain sentences only.\n\n"
176
+ f"{facts}\n\nExplanation:"
177
+ )
178
+
179
+ gen = explainer(
180
+ prompt,
181
+ max_new_tokens=140,
182
+ do_sample=False, # deterministic
183
+ num_beams=4,
184
+ early_stopping=True
185
+ )[0]["generated_text"]
186
+
187
+ cleaned = sanitize_to_plain_sentences(gen)
188
+ # If the model still produced garbage, fall back to a deterministic template.
189
+ if len(cleaned.split()) < 25: # too short / likely sanitized away
190
+ cleaned = fallback_explanation(payload)
191
+ return cleaned
192
+
193
+ # --- Gradio app (no JSON panel; with clickable examples) ---
194
+ def app_fn(driver_teeth, driven_teeth, input_torque, input_speed, efficiency):
195
+ result = validate_and_compute(
196
+ int(driver_teeth),
197
+ int(driven_teeth),
198
+ float(input_torque),
199
+ float(input_speed),
200
+ float(efficiency),
201
+ )
202
+
203
+ if not result["ok"]:
204
+ errors_md = "### Validation Errors\n" + "\n".join([f"- {e}" for e in result["errors"]])
205
+ return errors_md, "—"
206
+
207
+ r = result
208
+ def fmt(x, digits=6): # compact pretty printer
209
+ return f"{x:.{digits}f}".rstrip('0').rstrip('.') if isinstance(x, float) else str(x)
210
+
211
+ md = f"""
212
+ ### Numerical Results
213
+ - **Gear ratio (driven/driver):** {fmt(r['intermediate']['gear_ratio'], 6)}
214
+ - **Output speed:** {fmt(r['outputs']['output_speed_rpm'], 6)} rpm _(speed {r['outputs']['qualitative']['speed_change']})_
215
+ - **Output torque:** {fmt(r['outputs']['output_torque_Nm'], 6)} N·m _(torque {r['outputs']['qualitative']['torque_change']})_
216
+ - **Input power:** {fmt(r['outputs']['input_power_W'], 6)} W
217
+ - **Output power:** {fmt(r['outputs']['output_power_W'], 6)} W
218
+ - **Expected output power (Pin×η):** {fmt(r['outputs']['expected_output_power_W'], 6)} W
219
+ - **Power balance error:** {fmt(r['outputs']['power_balance_error_percent'], 6)} %
220
+ """
221
+ explanation = render_explanation(result)
222
+ return md, explanation
223
+
224
+ with gr.Blocks(title="Deterministic Gear Calculator (See the Math)") as demo:
225
+ gr.Markdown("# Deterministic Gear Calculator — with Natural-Language Explanation")
226
+ gr.Markdown(
227
+ "Two-gear train (driver → driven). Deterministic math with validation and a plain-English explanation."
228
+ )
229
+ with gr.Row():
230
+ with gr.Column():
231
+ in_driver = gr.Number(label="Driver gear teeth", value=20, precision=0)
232
+ in_driven = gr.Number(label="Driven gear teeth", value=65, precision=0)
233
+ in_torque = gr.Number(label="Input torque [N·m]", value=12.0)
234
+ in_speed = gr.Number(label="Input speed [rpm]", value=1800.0)
235
+ in_eta = gr.Slider(50, 100, value=97.0, step=0.1, label="Mesh efficiency [%]")
236
+ run_btn = gr.Button("Calculate", variant="primary")
237
+ with gr.Column():
238
+ results_md = gr.Markdown(label="Numerical Results")
239
+ explanation_md = gr.Markdown(label="Explain the results")
240
+
241
+ gr.Markdown("### Examples (click to fill)")
242
+ examples = [
243
+ [20, 60, 10.0, 1800.0, 97.0], # 3:1 reduction
244
+ [18, 36, 5.0, 1500.0, 96.0], # 2:1 reduction
245
+ [32, 24, 8.0, 1200.0, 95.0], # overdrive
246
+ [25, 75, 15.0, 3600.0, 98.0], # 3:1 with higher speed
247
+ ]
248
+ gr.Examples(
249
+ examples=examples,
250
+ inputs=[in_driver, in_driven, in_torque, in_speed, in_eta],
251
+ label="Common scenarios"
252
+ )
253
+
254
+ run_btn.click(
255
+ app_fn,
256
+ [in_driver, in_driven, in_torque, in_speed, in_eta],
257
+ [results_md, explanation_md]
258
+ )
259
+
260
+ demo.launch()