gears / app.py
scottymcgee's picture
Create app.py
5ebc3d3 verified
# --- deps (minimal) ---
# !pip -q install "gradio>=4.44.0" "transformers>=4.42.0"
# --- deterministic backend ---
from math import pi
import re
import gradio as gr
from transformers import pipeline
def validate_and_compute(
driver_teeth:int,
driven_teeth:int,
input_torque_Nm:float,
input_speed_rpm:float,
efficiency_pct:float,
):
"""
Deterministic, first-principles 2-gear calculator.
SCOPE & ASSUMPTIONS
- Two rigid spur gears (driver -> driven), single mesh, no slippage.
- Quasi-steady operation; efficiency is mesh efficiency only.
- No shaft/bearing losses; no strength checks.
INPUTS & VALID RANGES
- driver_teeth: 6..400 (int)
- driven_teeth: 6..400 (int)
- input_torque_Nm: 0..10_000 [N·m]
- input_speed_rpm: 0..30_000 [rpm]
- efficiency_pct: 50..100 [%]
"""
problems = []
if not isinstance(driver_teeth, int) or not (6 <= driver_teeth <= 400):
problems.append("driver_teeth must be an integer in [6, 400].")
if not isinstance(driven_teeth, int) or not (6 <= driven_teeth <= 400):
problems.append("driven_teeth must be an integer in [6, 400].")
if not (0 <= input_torque_Nm <= 10_000):
problems.append("input_torque_Nm must be in [0, 10_000] N·m.")
if not (0 <= input_speed_rpm <= 30_000):
problems.append("input_speed_rpm must be in [0, 30_000] rpm.")
if not (50 <= efficiency_pct <= 100):
problems.append("efficiency_pct must be in [50, 100] %.")
if problems:
return {"ok": False, "errors": problems}
gear_ratio = driven_teeth / driver_teeth # >1 → torque↑ speed↓
eta = efficiency_pct / 100.0
# kinematics
output_speed_rpm = input_speed_rpm / gear_ratio
# torque (power*eta conserved across mesh)
output_torque_Nm = input_torque_Nm * gear_ratio * eta
# power check
omega_in = input_speed_rpm * (2*pi/60.0) # rad/s
omega_out = output_speed_rpm * (2*pi/60.0) # rad/s
pin_W = input_torque_Nm * omega_in
pout_W = output_torque_Nm * omega_out
expected_pout_W = pin_W * eta
power_balance_error_pct = (0 if expected_pout_W == 0 else
100.0 * (pout_W - expected_pout_W) / max(1e-12, expected_pout_W))
speed_change = "decrease" if gear_ratio > 1 else ("increase" if gear_ratio < 1 else "no change")
torque_change = "increase" if gear_ratio > 1 else ("decrease" if gear_ratio < 1 else "no change")
return {
"ok": True,
"inputs": {
"driver_teeth": driver_teeth,
"driven_teeth": driven_teeth,
"input_torque_Nm": float(input_torque_Nm),
"input_speed_rpm": float(input_speed_rpm),
"efficiency_pct": float(efficiency_pct)
},
"intermediate": {
"gear_ratio": float(gear_ratio),
"eta": float(eta)
},
"outputs": {
"output_speed_rpm": float(output_speed_rpm),
"output_torque_Nm": float(output_torque_Nm),
"input_power_W": float(pin_W),
"output_power_W": float(pout_W),
"expected_output_power_W": float(expected_pout_W),
"power_balance_error_percent": float(power_balance_error_pct),
"qualitative": {
"speed_change": speed_change,
"torque_change": torque_change
}
}
}
# --- compact, deterministic LLM explainer ---
explainer = pipeline("text2text-generation", model="google/flan-t5-small")
def sanitize_to_plain_sentences(text: str, max_sentences: int = 6) -> str:
"""Drop code/JSON-y lines and keep up to N plain sentences."""
# Strip code blocks/backticks
text = re.sub(r"```.*?```", "", text, flags=re.S)
text = text.replace("`", "")
# Keep only part after the last 'Explanation:' if present
if "Explanation:" in text:
text = text.split("Explanation:")[-1]
# Remove obvious code-ish lines
cleaned_lines = []
for line in text.splitlines():
l = line.strip()
if not l:
continue
if re.search(r"\binput\(", l) or re.search(r"\bprint\(", l):
continue
if re.match(r"^\s*[A-Za-z_]\w*\s*=", l): # assignments
continue
if l.startswith("{") or l.startswith("[") or l.endswith("}"):
continue
cleaned_lines.append(l)
text = " ".join(cleaned_lines)
# Split into sentences conservatively
# (avoid splitting inside units like N·m, rpm—these don’t have periods)
sentences = re.split(r"(?<=[.!?])\s+", text)
# Trim and keep sentences that look like natural language
plain = []
for s in sentences:
s = s.strip()
if not s:
continue
# Heuristics to avoid code/fragments
if re.search(r"[{};]|==|!=|<=|>=|\breturn\b|\bdef\b|\bclass\b", s):
continue
plain.append(s)
if len(plain) >= max_sentences:
break
return " ".join(plain).strip()
def fallback_explanation(payload: dict) -> str:
"""Deterministic template if the LLM output gets sanitized to nothing/too short."""
inp = payload["inputs"]; mid = payload["intermediate"]; out = payload["outputs"]
sc = out["qualitative"]["speed_change"]; tc = out["qualitative"]["torque_change"]
return (
f"The driven gear has {inp['driven_teeth']} teeth and the driver has {inp['driver_teeth']}, "
f"so the gear ratio is {mid['gear_ratio']:.3f} (driven/driver). "
f"With an input torque of {inp['input_torque_Nm']:.3g} N·m at {inp['input_speed_rpm']:.3g} rpm, "
f"the output speed becomes {out['output_speed_rpm']:.3g} rpm (a {sc} in speed) and the output torque is "
f"{out['output_torque_Nm']:.3g} N·m (a {tc} in torque). "
f"Input power is {out['input_power_W']:.3g} W; with an efficiency of {inp['efficiency_pct']:.3g}%, "
f"the expected output power is {out['expected_output_power_W']:.3g} W, which matches the computed "
f"{out['output_power_W']:.3g} W. "
f"This calculation considers mesh efficiency only; check tooth strength, shafts, and bearings separately."
)
def render_explanation(payload: dict) -> str:
if not payload.get("ok"):
return "There were validation errors; please fix inputs and try again."
inp = payload["inputs"]; mid = payload["intermediate"]; out = payload["outputs"]
# Only pass essentials so the model can’t riff on raw JSON.
facts = (
f"Driver teeth: {inp['driver_teeth']}; Driven teeth: {inp['driven_teeth']}. "
f"Input torque: {inp['input_torque_Nm']:.4g} N·m; Input speed: {inp['input_speed_rpm']:.4g} rpm. "
f"Efficiency: {inp['efficiency_pct']:.4g}%. "
f"Gear ratio (driven/driver): {mid['gear_ratio']:.6g}. "
f"Output speed: {out['output_speed_rpm']:.6g} rpm; Output torque: {out['output_torque_Nm']:.6g} N·m. "
f"Input power: {out['input_power_W']:.6g} W; Output power: {out['output_power_W']:.6g} W; "
f"Expected Pin×η: {out['expected_output_power_W']:.6g} W."
)
prompt = (
"You are a careful engineering assistant. Using only the facts below, write 4–6 clear sentences that "
"explain the gear calculation to a non-expert. Mention the gear ratio’s tradeoff (speed vs torque) and "
"comment that output power is approximately input power times efficiency. "
"Do NOT include code, equations, lists, JSON, or symbols like '=', '{', '}'. Plain sentences only.\n\n"
f"{facts}\n\nExplanation:"
)
gen = explainer(
prompt,
max_new_tokens=140,
do_sample=False, # deterministic
num_beams=4,
early_stopping=True
)[0]["generated_text"]
cleaned = sanitize_to_plain_sentences(gen)
# If the model still produced garbage, fall back to a deterministic template.
if len(cleaned.split()) < 25: # too short / likely sanitized away
cleaned = fallback_explanation(payload)
return cleaned
# --- Gradio app (no JSON panel; with clickable examples) ---
def app_fn(driver_teeth, driven_teeth, input_torque, input_speed, efficiency):
result = validate_and_compute(
int(driver_teeth),
int(driven_teeth),
float(input_torque),
float(input_speed),
float(efficiency),
)
if not result["ok"]:
errors_md = "### Validation Errors\n" + "\n".join([f"- {e}" for e in result["errors"]])
return errors_md, "—"
r = result
def fmt(x, digits=6): # compact pretty printer
return f"{x:.{digits}f}".rstrip('0').rstrip('.') if isinstance(x, float) else str(x)
md = f"""
### Numerical Results
- **Gear ratio (driven/driver):** {fmt(r['intermediate']['gear_ratio'], 6)}
- **Output speed:** {fmt(r['outputs']['output_speed_rpm'], 6)} rpm _(speed {r['outputs']['qualitative']['speed_change']})_
- **Output torque:** {fmt(r['outputs']['output_torque_Nm'], 6)} N·m _(torque {r['outputs']['qualitative']['torque_change']})_
- **Input power:** {fmt(r['outputs']['input_power_W'], 6)} W
- **Output power:** {fmt(r['outputs']['output_power_W'], 6)} W
- **Expected output power (Pin×η):** {fmt(r['outputs']['expected_output_power_W'], 6)} W
- **Power balance error:** {fmt(r['outputs']['power_balance_error_percent'], 6)} %
"""
explanation = render_explanation(result)
return md, explanation
with gr.Blocks(title="Deterministic Gear Calculator (See the Math)") as demo:
gr.Markdown("# Deterministic Gear Calculator — with Natural-Language Explanation")
gr.Markdown(
"Two-gear train (driver → driven). Deterministic math with validation and a plain-English explanation."
)
with gr.Row():
with gr.Column():
in_driver = gr.Number(label="Driver gear teeth", value=20, precision=0)
in_driven = gr.Number(label="Driven gear teeth", value=65, precision=0)
in_torque = gr.Number(label="Input torque [N·m]", value=12.0)
in_speed = gr.Number(label="Input speed [rpm]", value=1800.0)
in_eta = gr.Slider(50, 100, value=97.0, step=0.1, label="Mesh efficiency [%]")
run_btn = gr.Button("Calculate", variant="primary")
with gr.Column():
results_md = gr.Markdown(label="Numerical Results")
explanation_md = gr.Markdown(label="Explain the results")
gr.Markdown("### Examples (click to fill)")
examples = [
[20, 60, 10.0, 1800.0, 97.0], # 3:1 reduction
[18, 36, 5.0, 1500.0, 96.0], # 2:1 reduction
[32, 24, 8.0, 1200.0, 95.0], # overdrive
[25, 75, 15.0, 3600.0, 98.0], # 3:1 with higher speed
]
gr.Examples(
examples=examples,
inputs=[in_driver, in_driven, in_torque, in_speed, in_eta],
label="Common scenarios"
)
run_btn.click(
app_fn,
[in_driver, in_driven, in_torque, in_speed, in_eta],
[results_md, explanation_md]
)
demo.launch()