streamlit-delta-PUU-calculator/main.py

1091 lines
38 KiB
Python
Raw Normal View History

2026-01-06 17:20:57 +00:00
# app.py
# Streamlit: Delta ASDA-A2 Engineering Tool (PUU + validation + speed/torque + templates + report export)
#
# Run:
# streamlit run app.py
import io
import math
from dataclasses import dataclass
from datetime import datetime
2026-01-06 17:05:40 +00:00
from math import gcd
2026-01-06 17:20:57 +00:00
import streamlit as st
# Optional PDF export (reportlab is installed in your environment)
try:
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
2026-01-06 17:20:57 +00:00
REPORTLAB_OK = True
except Exception:
REPORTLAB_OK = False
2026-01-06 17:05:40 +00:00
# ----------------------------
# Helpers
# ----------------------------
def simplify_fraction(n: int, d: int) -> tuple[int, int]:
if d == 0:
raise ValueError("Denominator cannot be 0.")
if n == 0:
return 0, 1
g = gcd(abs(n), abs(d))
n //= g
d //= g
if d < 0:
n *= -1
d *= -1
return n, d
def scale_down_to_limit(n: int, d: int, limit: int) -> tuple[int, int]:
2026-01-06 17:20:57 +00:00
"""Scale down proportionally if n or d exceed limit; keep ratio approx; re-simplify."""
if d == 0:
raise ValueError("Denominator cannot be 0.")
2026-01-06 17:05:40 +00:00
if n == 0:
return 0, 1
n_abs, d_abs = abs(n), abs(d)
if n_abs <= limit and d_abs <= limit:
return n, d
factor = max((n_abs + limit - 1) // limit, (d_abs + limit - 1) // limit)
n2 = n // factor
d2 = d // factor
if n2 == 0:
n2 = 1 if n > 0 else -1
if d2 == 0:
d2 = 1
return simplify_fraction(n2, d2)
2026-01-06 17:20:57 +00:00
def ratio_error_ppm(target: float, chosen: float) -> float:
if target == 0:
return 0.0 if chosen == 0 else float("inf")
return (chosen - target) / target * 1_000_000.0
def format_ppm(ppm: float) -> str:
if math.isinf(ppm):
return ""
return f"{ppm:.2f} ppm"
def clamp_float(x: float, lo: float, hi: float) -> float:
return max(lo, min(hi, x))
# ---- SI-style numeric input (Slovenia formatting) ----
def format_si_number(x: float, decimals: int = 0) -> str:
"""Format number like 360.000 (thousands='.', decimal=',')."""
if x is None:
return ""
sign = "-" if x < 0 else ""
x = abs(float(x))
p = 10 ** decimals
x = round(x * p) / p
int_part = int(x)
frac_part = x - int_part
int_str = f"{int_part:,}".replace(",", ".") # thousands
if decimals <= 0:
return f"{sign}{int_str}"
frac_str = f"{frac_part:.{decimals}f}".split(".")[1]
return f"{sign}{int_str},{frac_str}"
def parse_si_number(s: str, allow_float: bool = True) -> float:
"""
Accept:
360000
360.000
360 000
360.000,25
360000.25
"""
if s is None:
raise ValueError("Empty")
t = s.strip().replace(" ", "")
if t == "":
raise ValueError("Empty")
if "," in t:
# decimal comma -> remove thousands '.' and convert comma to '.'
t = t.replace(".", "")
t = t.replace(",", ".")
else:
# no comma: dot might be thousands or decimal
if "." in t:
last = t.split(".")[-1]
if len(last) == 3 and last.isdigit():
t = t.replace(".", "") # thousands separators
# else keep '.' as decimal point
val = float(t)
if not allow_float and abs(val - int(val)) > 1e-12:
raise ValueError("Integer required")
return float(int(val)) if not allow_float else val
def si_number_input(
label: str,
key: str,
value: float,
decimals: int = 0,
allow_float: bool = False,
min_value: float | None = None,
help: str | None = None,
):
txt_key = f"{key}__txt"
err_key = f"{key}__err"
# init state
if key not in st.session_state:
st.session_state[key] = value
if txt_key not in st.session_state:
st.session_state[txt_key] = format_si_number(value, decimals)
if err_key not in st.session_state:
st.session_state[err_key] = ""
def _commit():
raw = st.session_state.get(txt_key, "")
try:
v = parse_si_number(raw, allow_float=allow_float)
if min_value is not None and v < min_value:
raise ValueError(f"Must be ≥ {min_value}")
st.session_state[key] = v
st.session_state[err_key] = ""
# ✅ normalize display to SI formatting
st.session_state[txt_key] = format_si_number(v, decimals)
except Exception as e:
st.session_state[err_key] = str(e)
st.text_input(label, key=txt_key, help=help, on_change=_commit)
# show error (if any)
if st.session_state.get(err_key):
st.error(f"Invalid number for '{label}': {st.session_state[err_key]}")
return st.session_state[key]
2026-01-06 17:20:57 +00:00
def build_markdown_report(data: dict) -> str:
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines = []
lines.append("# Delta ASDA-A2 Commissioning Report")
2026-01-06 17:20:57 +00:00
lines.append("")
lines.append(f"- Generated: **{ts}**")
lines.append("")
lines.append("## Inputs")
for k, v in data["inputs"].items():
lines.append(f"- **{k}:** {v}")
lines.append("")
lines.append("## PUU / Electronic Gear Result")
for k, v in data["puu"].items():
lines.append(f"- **{k}:** {v}")
lines.append("")
lines.append("## Sanity Checks")
for k, v in data["sanity"].items():
lines.append(f"- **{k}:** {v}")
lines.append("")
lines.append("## Validations")
if not data["validations"]:
lines.append("- (none)")
else:
for item in data["validations"]:
lines.append(f"- {item}")
lines.append("")
lines.append("## Speed Translator")
for k, v in data["speed"].items():
lines.append(f"- **{k}:** {v}")
lines.append("")
lines.append("## Torque Estimator")
for k, v in data["torque"].items():
lines.append(f"- **{k}:** {v}")
lines.append("")
lines.append("## Parameter Templates")
for name, items in data["templates"].items():
lines.append(f"### {name}")
for line in items:
lines.append(f"- {line}")
lines.append("")
return "\n".join(lines)
def markdown_to_pdf_bytes(title: str, md_text: str) -> bytes:
"""
Minimal PDF exporter:
- Not a full markdown renderer; just prints lines (wraps long lines).
- Works well for a commissioning report.
"""
if not REPORTLAB_OK:
raise RuntimeError("reportlab not available")
buffer = io.BytesIO()
c = canvas.Canvas(buffer, pagesize=A4)
width, height = A4
margin = 40
y = height - margin
line_height = 12
max_width_chars = 105 # rough wrap; avoids needing font metrics
c.setTitle(title)
c.setFont("Helvetica", 11)
def new_page():
nonlocal y
c.showPage()
c.setFont("Helvetica", 11)
y = height - margin
for raw in md_text.splitlines():
line = raw.rstrip("\n")
if not line:
y -= line_height
if y < margin:
new_page()
continue
chunks = [line[i : i + max_width_chars] for i in range(0, len(line), max_width_chars)]
2026-01-06 17:20:57 +00:00
for chunk in chunks:
c.drawString(margin, y, chunk)
y -= line_height
if y < margin:
new_page()
c.save()
buffer.seek(0)
return buffer.read()
# ----------------------------
# Domain logic
# ----------------------------
@dataclass
class PUUResult:
n_exact: int
d_exact: int
n_final: int
d_final: int
target_ratio: float
chosen_ratio: float
error_ppm: float
def compute_puu(
motor_counts_per_rev: float,
gear_ratio: float,
units_per_load_rev: float,
controller_pulses_per_unit: float,
ratio_limit: int,
) -> PUUResult:
if motor_counts_per_rev <= 0:
raise ValueError("motor_counts_per_rev must be > 0")
if gear_ratio <= 0:
raise ValueError("gear_ratio must be > 0")
if units_per_load_rev <= 0:
raise ValueError("units_per_load_rev must be > 0")
if controller_pulses_per_unit <= 0:
raise ValueError("controller_pulses_per_unit must be > 0")
if ratio_limit < 1000:
raise ValueError("ratio_limit too small")
motor_counts_per_load_rev = motor_counts_per_rev * gear_ratio
denom_units = units_per_load_rev * controller_pulses_per_unit
target = motor_counts_per_load_rev / denom_units
# convert to integer fraction with scaling for decimals
SCALE = 1_000_000
n_raw = int(round(motor_counts_per_load_rev * SCALE))
d_raw = int(round(denom_units * SCALE))
n_exact, d_exact = simplify_fraction(n_raw, d_raw)
n_final, d_final = scale_down_to_limit(n_exact, d_exact, int(ratio_limit))
chosen = n_final / d_final
err = ratio_error_ppm(target, chosen)
return PUUResult(
n_exact=n_exact,
d_exact=d_exact,
n_final=n_final,
d_final=d_final,
target_ratio=target,
chosen_ratio=chosen,
error_ppm=err,
)
def validate_delta_limits(
ratio: float,
min_ratio: float,
max_ratio: float,
n: int,
d: int,
int_limit: int,
) -> list[str]:
issues = []
if ratio < min_ratio:
issues.append(f"❌ Electronic gear ratio {ratio:.6f} is **below** Delta limit {min_ratio:.6f}.")
elif ratio > max_ratio:
issues.append(f"❌ Electronic gear ratio {ratio:.6f} is **above** Delta limit {max_ratio:.6f}.")
else:
issues.append(f"✅ Electronic gear ratio {ratio:.6f} is within Delta limit [{min_ratio:.6f}, {max_ratio:.6f}].")
if abs(n) > int_limit or abs(d) > int_limit:
issues.append(f"❌ P1-44/45 exceed integer limit {int_limit}.")
else:
issues.append(f"✅ P1-44/45 are within integer limit {int_limit}.")
if d == 0:
issues.append("❌ Denominator (P1-45) is 0 (invalid).")
elif d < 0:
issues.append("⚠️ Denominator is negative (we usually keep it positive).")
return issues
def suggestions_if_out_of_range(ratio: float, min_ratio: float, max_ratio: float) -> list[str]:
sugg = []
if ratio < min_ratio:
sugg.append("Try **increasing** ratio by:")
sugg.append("- Increasing motor_counts_per_rev (if encoder mode supports it)")
sugg.append("- Increasing gear_ratio (if you entered it wrong)")
sugg.append("- Decreasing units_per_load_rev (e.g., use turns instead of degrees)")
sugg.append("- Decreasing controller_pulses_per_unit (controller scaling)")
elif ratio > max_ratio:
sugg.append("Try **decreasing** ratio by:")
sugg.append("- Increasing units_per_load_rev (degrees→micro-degrees, or mm→0.1mm units)")
sugg.append("- Increasing controller_pulses_per_unit (more pulses per user unit)")
sugg.append("- Decreasing motor_counts_per_rev (if encoder mode supports it)")
return sugg
def calc_speed_translator(
gear_ratio: float,
units_per_load_rev: float,
motor_rpm: float | None,
load_rpm: float | None,
) -> dict:
# Relations: motor_rpm = load_rpm * gear_ratio
if motor_rpm is None and load_rpm is None:
load_rpm = 10.0
if motor_rpm is None:
motor_rpm = load_rpm * gear_ratio
if load_rpm is None:
load_rpm = motor_rpm / gear_ratio
load_rps = load_rpm / 60.0
motor_rps = motor_rpm / 60.0
units_per_sec = load_rps * units_per_load_rev
units_per_min = load_rpm * units_per_load_rev
return {
"Motor RPM": motor_rpm,
"Load RPM": load_rpm,
"Motor RPS": motor_rps,
"Load RPS": load_rps,
"User units / s": units_per_sec,
"User units / min": units_per_min,
}
def calc_torque_estimator(
motor_rated_nm: float,
motor_peak_nm: float,
gear_ratio: float,
efficiency: float,
) -> dict:
efficiency = clamp_float(efficiency, 0.01, 1.0)
out_rated = motor_rated_nm * gear_ratio * efficiency
out_peak = motor_peak_nm * gear_ratio * efficiency
return {
"Efficiency": efficiency,
"Estimated output rated torque (Nm)": out_rated,
"Estimated output peak torque (Nm)": out_peak,
}
def template_blocks() -> dict[str, list[str]]:
return {
"First power-on (SAFE)": [
"P1-02 Speed Limit: low (e.g., 300800 RPM) for first tests",
"P1-12 / P1-13 Torque Limit: 3060% to avoid end-stop damage",
"P1-44 / P1-45: from PUU calculator",
"Verify direction with short jog",
"Verify 1 load rev equals expected user units (e.g., 360°)",
],
"Tuning mode": [
"P1-37 Load/Motor inertia: start 1020 for gearbox systems",
"Run auto-tune (if available) at low risk speed",
"Increase torque limit step-by-step only if needed",
],
"Production (NORMAL)": [
"P1-02 Speed Limit: motor-rated safe speed (e.g., 3000 RPM) or machine limit",
"Torque limits set to protect mechanics and process",
"Store backup of parameters / report",
],
}
2026-01-06 17:05:40 +00:00
# ----------------------------
2026-01-06 17:20:57 +00:00
# Streamlit UI state
2026-01-06 17:05:40 +00:00
# ----------------------------
st.set_page_config(page_title="Delta ASDA-A2 Engineering Tool", layout="wide")
2026-01-06 17:20:57 +00:00
defaults = {
"motor_counts_per_rev": 128000.0,
"gear_ratio": 50.0,
"unit_preset": "Degrees at output (360 units / load rev)",
"units_per_load_rev": 360.0,
"lead_mm_per_rev": 5.0,
"pulley_circum_mm": 100.0,
"controller_pulses_per_unit": 1.0,
"ratio_limit": 2_147_483_647,
"delta_min_ratio": 1.0 / 50.0, # you referenced 1/50
"delta_max_ratio": 25600.0, # you referenced 25600
2026-01-06 17:20:57 +00:00
"smooth_min_counts_per_user_unit": 5000.0,
"motor_rated_rpm": 3000.0,
"motor_rated_torque_nm": 1.27,
"motor_peak_torque_nm": 3.82,
"gear_efficiency": 0.90,
"speed_input_mode": "Load RPM → Motor RPM",
"load_rpm_in": 10.0,
"motor_rpm_in": 500.0,
}
for k, v in defaults.items():
st.session_state.setdefault(k, v)
2026-01-06 17:05:40 +00:00
st.sidebar.title("Configuration")
page = st.sidebar.radio(
"Go to",
2026-01-06 17:20:57 +00:00
[
"Theory & Architecture",
"PUU Calculator",
"Speed Translator",
"Torque Estimator",
"Parameter Guide",
"Commissioning Checklist",
"ASDA-Soft Guide",
"Export Report",
],
2026-01-06 17:05:40 +00:00
)
2026-01-06 17:20:57 +00:00
with st.sidebar:
if st.button("Reset all inputs"):
# reset numeric values
2026-01-06 17:20:57 +00:00
for k, v in defaults.items():
st.session_state[k] = v
# reset text inputs used by si_number_input
for k in list(st.session_state.keys()):
if k.endswith("__txt"):
del st.session_state[k]
2026-01-06 17:20:57 +00:00
st.rerun()
2026-01-06 17:05:40 +00:00
# ----------------------------
# Pages
# ----------------------------
if page == "Theory & Architecture":
st.title("📚 Theory & Architecture")
st.markdown("### What this tool does")
st.write(
"Helps you set **electronic gearing (PUU / P1-44 & P1-45)** and perform quick "
"**speed/torque sanity checks** for gearbox systems."
)
2026-01-06 17:05:40 +00:00
st.markdown("### Core equation")
st.latex(r"\text{Motor encoder counts} = \text{Command pulses} \times \frac{P1\text{-}44}{P1\text{-}45}")
2026-01-06 17:05:40 +00:00
st.markdown("### With a gearbox")
st.markdown(
"- Motor counts per **load revolution** = (motor counts / motor rev) × (gear ratio)\n"
"- Choose **user units per load revolution** (example: **360** for degrees)\n"
"- Include **controller pulses per user unit** if your controller scales commands"
)
st.latex(
r"\frac{P1\text{-}44}{P1\text{-}45}="
r"\frac{\text{motor counts per load rev}}{\text{units per load rev}\times\text{controller pulses per unit}}"
)
2026-01-06 17:05:40 +00:00
st.markdown("### What “smoothness” means (practically)")
st.write(
"If **counts per user unit** is too low, motion can feel “steppy” at low speed. "
"This app shows **counts/user-unit** and warns if you drop below your threshold."
)
st.info(
"Tip: If you command in **degrees**, 360 units/rev is a clean choice. "
"If ratio becomes too large/small, scale units (e.g., 0.1° or 0.01°)."
2026-01-06 17:05:40 +00:00
)
elif page == "PUU Calculator":
st.title("🔢 Universal PUU Calculator (P1-44 / P1-45)")
st.caption(
2026-01-06 17:20:57 +00:00
"Compute electronic gearing for Delta ASDA-A2. Includes presets, exact vs drive-friendly fraction, and validations."
2026-01-06 17:05:40 +00:00
)
col1, col2 = st.columns(2)
with col1:
st.subheader("Input")
2026-01-06 17:20:57 +00:00
# BIG numbers => SI formatting (360.000 style)
st.session_state.motor_counts_per_rev = si_number_input(
2026-01-06 17:05:40 +00:00
"Motor encoder counts per motor revolution",
key="motor_counts_per_rev",
2026-01-06 17:20:57 +00:00
value=float(st.session_state.motor_counts_per_rev),
decimals=0,
allow_float=False,
min_value=1,
2026-01-06 17:20:57 +00:00
help="Examples: 128000, 131072, 1048576 (depends on encoder + drive setting).",
2026-01-06 17:05:40 +00:00
)
# Smaller engineering values can remain number_input
2026-01-06 17:20:57 +00:00
st.session_state.gear_ratio = st.number_input(
2026-01-06 17:05:40 +00:00
"Gear ratio (motor rev per load rev)",
2026-01-06 17:20:57 +00:00
min_value=0.0001,
value=float(st.session_state.gear_ratio),
2026-01-06 17:05:40 +00:00
step=1.0,
2026-01-06 17:20:57 +00:00
help="50:1 reduction => enter 50.",
2026-01-06 17:05:40 +00:00
)
2026-01-06 17:20:57 +00:00
# Presets
preset = st.selectbox(
"User unit preset",
[
"Degrees at output (360 units / load rev)",
"Turns at output (1 unit / load rev)",
"Leadscrew linear mm (units = mm)",
"Belt/Pulley linear mm (units = mm)",
"Custom",
],
index=[
"Degrees at output (360 units / load rev)",
"Turns at output (1 unit / load rev)",
"Leadscrew linear mm (units = mm)",
"Belt/Pulley linear mm (units = mm)",
"Custom",
].index(st.session_state.unit_preset),
2026-01-06 17:05:40 +00:00
)
2026-01-06 17:20:57 +00:00
st.session_state.unit_preset = preset
if preset == "Degrees at output (360 units / load rev)":
st.session_state.units_per_load_rev = 360.0
elif preset == "Turns at output (1 unit / load rev)":
st.session_state.units_per_load_rev = 1.0
elif preset == "Leadscrew linear mm (units = mm)":
st.session_state.lead_mm_per_rev = st.number_input(
"Leadscrew lead (mm per load revolution)",
min_value=0.0001,
value=float(st.session_state.lead_mm_per_rev),
step=0.1,
)
st.session_state.units_per_load_rev = float(st.session_state.lead_mm_per_rev)
elif preset == "Belt/Pulley linear mm (units = mm)":
st.session_state.pulley_circum_mm = st.number_input(
"Pulley circumference (mm per load revolution)",
min_value=0.0001,
value=float(st.session_state.pulley_circum_mm),
step=1.0,
)
st.session_state.units_per_load_rev = float(st.session_state.pulley_circum_mm)
else:
st.session_state.units_per_load_rev = si_number_input(
"User units per load revolution",
key="units_per_load_rev",
value=float(st.session_state.units_per_load_rev),
decimals=0, # <-- shows 360 or 360.000, never 360.00
allow_float=False, # <-- integer only (degrees etc.)
min_value=1,
help="Example: 360 for degrees. You may type 360000 or 360.000 (same).",
)
2026-01-06 17:20:57 +00:00
st.session_state.controller_pulses_per_unit = st.number_input(
2026-01-06 17:05:40 +00:00
"Controller command pulses per user unit",
2026-01-06 17:20:57 +00:00
min_value=0.0001,
value=float(st.session_state.controller_pulses_per_unit),
2026-01-06 17:05:40 +00:00
step=1.0,
2026-01-06 17:20:57 +00:00
help="If 1 pulse = 1 user unit, keep 1. If 10 pulses per degree, enter 10.",
2026-01-06 17:05:40 +00:00
)
# BIG integer => SI formatting
st.session_state.ratio_limit = si_number_input(
2026-01-06 17:05:40 +00:00
"Max integer limit for P1-44 / P1-45 (safety)",
key="ratio_limit",
value=float(st.session_state.ratio_limit),
decimals=0,
allow_float=False,
2026-01-06 17:05:40 +00:00
min_value=1000,
)
2026-01-06 17:20:57 +00:00
with st.expander("Delta A2 ratio limits (editable)"):
st.session_state.delta_min_ratio = st.number_input(
"Min electronic gear ratio",
min_value=0.0,
value=float(st.session_state.delta_min_ratio),
step=0.0001,
format="%.6f",
)
st.session_state.delta_max_ratio = st.number_input(
"Max electronic gear ratio",
min_value=0.0,
value=float(st.session_state.delta_max_ratio),
step=100.0,
format="%.3f",
)
st.session_state.smooth_min_counts_per_user_unit = st.number_input(
"Recommended minimum counts per user unit (smoothness)",
min_value=1.0,
value=float(st.session_state.smooth_min_counts_per_user_unit),
step=100.0,
help="Rule of thumb to avoid stepping/vibration at low speeds.",
)
2026-01-06 17:05:40 +00:00
2026-01-06 17:20:57 +00:00
# Compute PUU
motor_counts_per_load_rev = float(st.session_state.motor_counts_per_rev) * float(st.session_state.gear_ratio)
denom_units = float(st.session_state.units_per_load_rev) * float(st.session_state.controller_pulses_per_unit)
2026-01-06 17:20:57 +00:00
target_ratio = motor_counts_per_load_rev / denom_units
try:
puu = compute_puu(
motor_counts_per_rev=float(st.session_state.motor_counts_per_rev),
gear_ratio=float(st.session_state.gear_ratio),
units_per_load_rev=float(st.session_state.units_per_load_rev),
controller_pulses_per_unit=float(st.session_state.controller_pulses_per_unit),
2026-01-06 17:20:57 +00:00
ratio_limit=int(st.session_state.ratio_limit),
)
except Exception as e:
st.error(f"PUU computation error: {e}")
st.stop()
counts_per_user_unit_load = motor_counts_per_load_rev / float(st.session_state.units_per_load_rev)
units_per_motor_rev = float(st.session_state.units_per_load_rev) / float(st.session_state.gear_ratio)
2026-01-06 17:20:57 +00:00
validations = validate_delta_limits(
ratio=puu.chosen_ratio,
min_ratio=float(st.session_state.delta_min_ratio),
max_ratio=float(st.session_state.delta_max_ratio),
n=puu.n_final,
d=puu.d_final,
int_limit=int(st.session_state.ratio_limit),
)
2026-01-06 17:05:40 +00:00
with col2:
st.subheader("Result")
2026-01-06 17:20:57 +00:00
m1, m2, m3 = st.columns(3)
m1.metric("P1-44 (N)", f"{puu.n_final}")
m2.metric("P1-45 (M)", f"{puu.d_final}")
m3.metric("Ratio (N/M)", f"{puu.chosen_ratio:.6f}")
2026-01-06 17:05:40 +00:00
2026-01-06 17:20:57 +00:00
st.markdown("### Exact vs drive-friendly")
st.write(f"- **Exact fraction:** {puu.n_exact} / {puu.d_exact}")
st.write(f"- **Final fraction:** {puu.n_final} / {puu.d_final}")
st.write(f"- **Target ratio:** {puu.target_ratio:.9f}")
st.write(f"- **Chosen ratio:** {puu.chosen_ratio:.9f}")
st.write(f"- **Error:** {format_ppm(puu.error_ppm)}")
2026-01-06 17:05:40 +00:00
2026-01-06 17:20:57 +00:00
st.markdown("### Meaning")
st.write(f"✅ **1 controller pulse → {puu.chosen_ratio:.6f} motor encoder counts**")
2026-01-06 17:05:40 +00:00
st.markdown("### Sanity checks")
st.write(f"- Motor counts per **load rev**: **{motor_counts_per_load_rev:.3f}**")
st.write(f"- User units per **load rev**: **{float(st.session_state.units_per_load_rev):.6f}**")
2026-01-06 17:20:57 +00:00
st.write(f"- Counts per **user unit** (load side): **{counts_per_user_unit_load:.3f}**")
st.write(f"- User units per **motor rev**: **{units_per_motor_rev:.6f}**")
if counts_per_user_unit_load >= float(st.session_state.smooth_min_counts_per_user_unit):
2026-01-06 17:20:57 +00:00
st.success(
f"Smoothness OK: counts/user-unit = {counts_per_user_unit_load:.1f}{float(st.session_state.smooth_min_counts_per_user_unit):.1f}"
2026-01-06 17:20:57 +00:00
)
else:
st.warning(
f"Low smoothness: counts/user-unit = {counts_per_user_unit_load:.1f} < {float(st.session_state.smooth_min_counts_per_user_unit):.1f}. "
2026-01-06 17:20:57 +00:00
"Consider increasing counts per unit (e.g., change pulses/unit, change unit scaling)."
)
st.markdown("### Validations")
for v in validations:
if v.startswith(""):
st.error(v)
elif v.startswith("⚠️"):
st.warning(v)
else:
st.success(v)
sugg = suggestions_if_out_of_range(
puu.chosen_ratio,
float(st.session_state.delta_min_ratio),
float(st.session_state.delta_max_ratio),
)
2026-01-06 17:20:57 +00:00
if sugg:
st.markdown("### Fix suggestions")
for s in sugg:
st.write(s)
st.divider()
st.markdown("### Quick copy")
txt = f"P1-44={puu.n_final}, P1-45={puu.d_final}"
st.code(txt)
st.components.v1.html(
f"""
<button onclick="navigator.clipboard.writeText('{txt}')"
style="padding:8px 12px;border-radius:8px;border:1px solid #888;cursor:pointer;">
Copy P1-44/P1-45 to clipboard
</button>
""",
height=55,
)
2026-01-06 17:05:40 +00:00
2026-01-06 17:20:57 +00:00
elif page == "Speed Translator":
st.title("⚡ Speed Translator (Motor ↔ Gearbox ↔ Load ↔ User Units)")
st.caption("Convert between motor RPM, load RPM, and user units/s based on gearbox and unit definition.")
col1, col2 = st.columns(2)
with col1:
st.subheader("Inputs")
st.session_state.gear_ratio = st.number_input(
"Gear ratio (motor rev per load rev)",
min_value=0.0001,
value=float(st.session_state.gear_ratio),
step=1.0,
2026-01-06 17:05:40 +00:00
)
2026-01-06 17:20:57 +00:00
st.session_state.units_per_load_rev = si_number_input(
2026-01-06 17:20:57 +00:00
"User units per load revolution",
key="units_per_load_rev",
2026-01-06 17:20:57 +00:00
value=float(st.session_state.units_per_load_rev),
decimals=0, # <-- shows 360 or 360.000, never 360.00
allow_float=False, # <-- integer only (degrees etc.)
min_value=1,
help="Example: 360 for degrees. You may type 360000 or 360.000 (same).",
2026-01-06 17:20:57 +00:00
)
st.session_state.motor_rated_rpm = st.number_input(
"Motor rated RPM (for warning)",
min_value=1.0,
value=float(st.session_state.motor_rated_rpm),
step=100.0,
)
st.session_state.speed_input_mode = st.radio(
"Input mode",
["Load RPM → Motor RPM", "Motor RPM → Load RPM"],
index=0 if st.session_state.speed_input_mode == "Load RPM → Motor RPM" else 1,
2026-01-06 17:05:40 +00:00
)
2026-01-06 17:20:57 +00:00
load_rpm = None
motor_rpm = None
if st.session_state.speed_input_mode == "Load RPM → Motor RPM":
st.session_state.load_rpm_in = st.number_input(
"Load RPM",
min_value=0.0,
value=float(st.session_state.load_rpm_in),
step=1.0,
)
load_rpm = float(st.session_state.load_rpm_in)
else:
st.session_state.motor_rpm_in = st.number_input(
"Motor RPM",
min_value=0.0,
value=float(st.session_state.motor_rpm_in),
step=10.0,
)
motor_rpm = float(st.session_state.motor_rpm_in)
speed = calc_speed_translator(
gear_ratio=float(st.session_state.gear_ratio),
units_per_load_rev=float(st.session_state.units_per_load_rev),
motor_rpm=motor_rpm,
load_rpm=load_rpm,
)
with col2:
st.subheader("Results")
a, b, c = st.columns(3)
a.metric("Motor RPM", f"{speed['Motor RPM']:.2f}")
b.metric("Load RPM", f"{speed['Load RPM']:.2f}")
c.metric("User units / s", f"{speed['User units / s']:.3f}")
st.write(f"- Motor RPS: **{speed['Motor RPS']:.4f}**")
st.write(f"- Load RPS: **{speed['Load RPS']:.4f}**")
st.write(f"- User units/min: **{speed['User units / min']:.3f}**")
if speed["Motor RPM"] > float(st.session_state.motor_rated_rpm):
st.error(f"❌ Motor RPM exceeds rated RPM: {speed['Motor RPM']:.1f} > {float(st.session_state.motor_rated_rpm):.1f}")
2026-01-06 17:20:57 +00:00
else:
st.success("✅ Motor RPM within rated RPM.")
elif page == "Torque Estimator":
st.title("🧲 Torque Estimator (Gearbox Amplification)")
st.caption("Quick estimator: output torque ≈ motor torque × gear ratio × efficiency.")
col1, col2 = st.columns(2)
with col1:
st.subheader("Inputs")
st.session_state.gear_ratio = st.number_input(
"Gear ratio (motor rev per load rev)",
min_value=0.0001,
value=float(st.session_state.gear_ratio),
step=1.0,
)
st.session_state.motor_rated_torque_nm = st.number_input(
"Motor rated torque (Nm)",
min_value=0.0,
value=float(st.session_state.motor_rated_torque_nm),
step=0.1,
)
st.session_state.motor_peak_torque_nm = st.number_input(
"Motor peak torque (Nm)",
min_value=0.0,
value=float(st.session_state.motor_peak_torque_nm),
step=0.1,
)
st.session_state.gear_efficiency = st.number_input(
"Gearbox efficiency (0..1)",
min_value=0.01,
max_value=1.0,
value=float(st.session_state.gear_efficiency),
step=0.01,
)
tor = calc_torque_estimator(
motor_rated_nm=float(st.session_state.motor_rated_torque_nm),
motor_peak_nm=float(st.session_state.motor_peak_torque_nm),
gear_ratio=float(st.session_state.gear_ratio),
efficiency=float(st.session_state.gear_efficiency),
)
with col2:
st.subheader("Results")
m1, m2 = st.columns(2)
m1.metric("Output rated torque (Nm)", f"{tor['Estimated output rated torque (Nm)']:.2f}")
m2.metric("Output peak torque (Nm)", f"{tor['Estimated output peak torque (Nm)']:.2f}")
st.info("Safety tip: during first tests, limit torque (P1-12/P1-13) because gearbox output torque can be huge.")
2026-01-06 17:05:40 +00:00
elif page == "Parameter Guide":
st.title("🛠️ Essential Parameter Setup")
2026-01-06 17:20:57 +00:00
st.write("Beyond PUU (P1-44/45), these are high-impact parameters for gearbox systems.")
2026-01-06 17:05:40 +00:00
st.subheader("1. The 'Big Three' for Gearbox Safety")
st.markdown(
"""
| Parameter | Name | Recommended Value | Why? |
| :--- | :--- | :--- | :--- |
2026-01-06 17:20:57 +00:00
| **P1-44** | Numerator (N) | *From Calculator* | Electronic gear / user units. |
| **P1-45** | Denominator (M) | *From Calculator* | Electronic gear / user units. |
| **P1-37** | Load to Motor Inertia | **10.0 ~ 20.0** | Gearbox reflects inertia to the motor; start here for tuning. |
2026-01-06 17:05:40 +00:00
"""
)
st.subheader("2. Speed & Torque Limits")
st.info("Gear reduction increases output torque and reduces output speed. Use limits to protect mechanics.")
2026-01-06 17:05:40 +00:00
st.markdown(
"""
2026-01-06 17:20:57 +00:00
- **P1-02 (Speed Limit):** Set to motor-rated speed or your machine limit.
- **P1-12 / P1-13 (Torque Limit):** Start conservative (3060%) for first movement tests.
2026-01-06 17:05:40 +00:00
"""
)
2026-01-06 17:20:57 +00:00
st.subheader("3. Parameter templates (copy/paste style)")
tpl = template_blocks()
for name, items in tpl.items():
with st.expander(name, expanded=False):
for line in items:
st.write(f"- {line}")
2026-01-06 17:05:40 +00:00
st.divider()
st.subheader("🔄 Factory Reset (Set to Default)")
st.warning("Warning: Resetting will erase all PUU and tuning settings!")
col_a, col_b = st.columns(2)
with col_a:
st.markdown("#### Via Drive Keypad")
st.code(
"""
1. Go to Parameter P2-08.
2. Set value to 10.
3. Press 'SET'.
4. Power Cycle (Off/On).
""".strip()
2026-01-06 17:05:40 +00:00
)
with col_b:
st.markdown("#### Via ASDA-Soft")
st.markdown(
"""
1. Open **Parameter Editor**.
2. Click **Initial** / **Factory Default**.
3. Confirm and click **Write**.
4. Restart the Drive.
"""
)
2026-01-06 17:20:57 +00:00
elif page == "Commissioning Checklist":
st.title("✅ Commissioning Checklist (Field Workflow)")
st.caption("A practical sequence to minimize mistakes and avoid mechanical damage.")
steps = [
("Factory reset if needed", "Reset P2-08=10 (or ASDA-Soft Initial) if you're unsure about previous settings."),
("Confirm encoder mode / counts", "Verify motor encoder counts per rev in drive settings."),
("Set gearbox ratio", "Confirm gear ratio direction and value (motor rev per load rev)."),
("Compute & set PUU", "Use PUU Calculator → write P1-44 and P1-45."),
("Set SAFE limits", "Set P1-02 low; set P1-12/13 torque limit 3060%."),
("Jog test (low speed)", "Short jog, verify direction and smoothness."),
("Verify scale", "One load revolution equals your expected user units (360° / mm etc.)."),
("Tune", "Set P1-37 and do auto-tune if used; increase limits gradually."),
("Production limits", "Set final speed/torque limits and safety interlocks."),
("Export report", "Use Export Report page to save settings for backup & documentation."),
]
for i, (title, detail) in enumerate(steps, 1):
checked = st.checkbox(f"{i}. {title}", key=f"chk_{i}")
if checked:
st.success(detail)
else:
st.write(detail)
2026-01-06 17:05:40 +00:00
elif page == "ASDA-Soft Guide":
st.title("💾 Implementation in ASDA-Soft")
st.markdown(
"""
To apply your **P1-44** and **P1-45** values:
1. Open **Parameter Editor**
2. Select **Group 1** (Basic Parameters)
3. Enter **P1-44** and **P1-45**
4. Click **Write to Drive**
5. **Power cycle / restart** the drive if required for the change to take effect
2026-01-06 17:20:57 +00:00
### Recommended workflow
- Compute PUU in **PUU Calculator**
2026-01-06 17:05:40 +00:00
- Set safe limits (**P1-02**, **P1-12**, **P1-13**)
2026-01-06 17:20:57 +00:00
- Verify motion direction and scaling
- Tune (**P1-37** and tuning flow)
2026-01-06 17:05:40 +00:00
"""
)
2026-01-06 17:20:57 +00:00
elif page == "Export Report":
st.title("🧾 Export Commissioning Report (Markdown / PDF)")
st.caption("Generates a report of your current inputs, PUU result, checks, speed and torque estimates.")
puu = compute_puu(
motor_counts_per_rev=float(st.session_state.motor_counts_per_rev),
gear_ratio=float(st.session_state.gear_ratio),
units_per_load_rev=float(st.session_state.units_per_load_rev),
controller_pulses_per_unit=float(st.session_state.controller_pulses_per_unit),
ratio_limit=int(st.session_state.ratio_limit),
)
motor_counts_per_load_rev = float(st.session_state.motor_counts_per_rev) * float(st.session_state.gear_ratio)
counts_per_user_unit_load = motor_counts_per_load_rev / float(st.session_state.units_per_load_rev)
units_per_motor_rev = float(st.session_state.units_per_load_rev) / float(st.session_state.gear_ratio)
validations = validate_delta_limits(
ratio=puu.chosen_ratio,
min_ratio=float(st.session_state.delta_min_ratio),
max_ratio=float(st.session_state.delta_max_ratio),
n=puu.n_final,
d=puu.d_final,
int_limit=int(st.session_state.ratio_limit),
)
sugg = suggestions_if_out_of_range(
puu.chosen_ratio,
float(st.session_state.delta_min_ratio),
float(st.session_state.delta_max_ratio),
)
speed = calc_speed_translator(
gear_ratio=float(st.session_state.gear_ratio),
units_per_load_rev=float(st.session_state.units_per_load_rev),
motor_rpm=float(st.session_state.motor_rpm_in) if st.session_state.speed_input_mode == "Motor RPM → Load RPM" else None,
load_rpm=float(st.session_state.load_rpm_in) if st.session_state.speed_input_mode == "Load RPM → Motor RPM" else None,
)
tor = calc_torque_estimator(
motor_rated_nm=float(st.session_state.motor_rated_torque_nm),
motor_peak_nm=float(st.session_state.motor_peak_torque_nm),
gear_ratio=float(st.session_state.gear_ratio),
efficiency=float(st.session_state.gear_efficiency),
)
report_data = {
"inputs": {
"Motor counts / rev": st.session_state.motor_counts_per_rev,
"Gear ratio (motor/load)": st.session_state.gear_ratio,
"User units / load rev": st.session_state.units_per_load_rev,
"Controller pulses / user unit": st.session_state.controller_pulses_per_unit,
"Integer limit": st.session_state.ratio_limit,
"Delta min ratio": st.session_state.delta_min_ratio,
"Delta max ratio": st.session_state.delta_max_ratio,
"Smooth min counts/user-unit": st.session_state.smooth_min_counts_per_user_unit,
"Motor rated RPM": st.session_state.motor_rated_rpm,
"Motor rated torque (Nm)": st.session_state.motor_rated_torque_nm,
"Motor peak torque (Nm)": st.session_state.motor_peak_torque_nm,
"Gear efficiency": st.session_state.gear_efficiency,
},
"puu": {
"P1-44 (N) final": puu.n_final,
"P1-45 (M) final": puu.d_final,
"Ratio (N/M)": f"{puu.chosen_ratio:.9f}",
"Exact fraction": f"{puu.n_exact}/{puu.d_exact}",
"Error": format_ppm(puu.error_ppm),
},
"sanity": {
"Motor counts per load rev": f"{motor_counts_per_load_rev:.3f}",
"Counts per user unit (load side)": f"{counts_per_user_unit_load:.3f}",
"User units per motor rev": f"{units_per_motor_rev:.6f}",
},
"validations": validations + (["\n".join(sugg)] if sugg else []),
"speed": {
"Motor RPM": f"{speed['Motor RPM']:.2f}",
"Load RPM": f"{speed['Load RPM']:.2f}",
"User units / s": f"{speed['User units / s']:.3f}",
"User units / min": f"{speed['User units / min']:.3f}",
},
"torque": {
"Efficiency": f"{tor['Efficiency']:.3f}",
"Output rated torque (Nm)": f"{tor['Estimated output rated torque (Nm)']:.2f}",
"Output peak torque (Nm)": f"{tor['Estimated output peak torque (Nm)']:.2f}",
},
"templates": template_blocks(),
}
md = build_markdown_report(report_data)
st.subheader("Preview")
st.markdown(md)
st.subheader("Download")
st.download_button(
"Download Markdown report (.md)",
data=md.encode("utf-8"),
file_name="asda_a2_commissioning_report.md",
mime="text/markdown",
)
if REPORTLAB_OK:
pdf_bytes = markdown_to_pdf_bytes("ASDA-A2 Commissioning Report", md)
st.download_button(
"Download PDF report (.pdf)",
data=pdf_bytes,
file_name="asda_a2_commissioning_report.pdf",
mime="application/pdf",
)
else:
st.info("PDF export unavailable (reportlab not installed in this environment).")