streamlit-delta-PUU-calculator/main.py

1091 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
from math import gcd
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
REPORTLAB_OK = True
except Exception:
REPORTLAB_OK = False
# ----------------------------
# 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]:
"""Scale down proportionally if n or d exceed limit; keep ratio approx; re-simplify."""
if d == 0:
raise ValueError("Denominator cannot be 0.")
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)
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]
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")
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)]
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",
],
}
# ----------------------------
# Streamlit UI state
# ----------------------------
st.set_page_config(page_title="Delta ASDA-A2 Engineering Tool", layout="wide")
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
"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)
st.sidebar.title("Configuration")
page = st.sidebar.radio(
"Go to",
[
"Theory & Architecture",
"PUU Calculator",
"Speed Translator",
"Torque Estimator",
"Parameter Guide",
"Commissioning Checklist",
"ASDA-Soft Guide",
"Export Report",
],
)
with st.sidebar:
if st.button("Reset all inputs"):
# reset numeric values
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]
st.rerun()
# ----------------------------
# 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."
)
st.markdown("### Core equation")
st.latex(r"\text{Motor encoder counts} = \text{Command pulses} \times \frac{P1\text{-}44}{P1\text{-}45}")
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}}"
)
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°)."
)
elif page == "PUU Calculator":
st.title("🔢 Universal PUU Calculator (P1-44 / P1-45)")
st.caption(
"Compute electronic gearing for Delta ASDA-A2. Includes presets, exact vs drive-friendly fraction, and validations."
)
col1, col2 = st.columns(2)
with col1:
st.subheader("Input")
# BIG numbers => SI formatting (360.000 style)
st.session_state.motor_counts_per_rev = si_number_input(
"Motor encoder counts per motor revolution",
key="motor_counts_per_rev",
value=float(st.session_state.motor_counts_per_rev),
decimals=0,
allow_float=False,
min_value=1,
help="Examples: 128000, 131072, 1048576 (depends on encoder + drive setting).",
)
# Smaller engineering values can remain number_input
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,
help="50:1 reduction => enter 50.",
)
# 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),
)
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).",
)
st.session_state.controller_pulses_per_unit = st.number_input(
"Controller command pulses per user unit",
min_value=0.0001,
value=float(st.session_state.controller_pulses_per_unit),
step=1.0,
help="If 1 pulse = 1 user unit, keep 1. If 10 pulses per degree, enter 10.",
)
# BIG integer => SI formatting
st.session_state.ratio_limit = si_number_input(
"Max integer limit for P1-44 / P1-45 (safety)",
key="ratio_limit",
value=float(st.session_state.ratio_limit),
decimals=0,
allow_float=False,
min_value=1000,
)
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.",
)
# 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)
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),
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)
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),
)
with col2:
st.subheader("Result")
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}")
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)}")
st.markdown("### Meaning")
st.write(f"✅ **1 controller pulse → {puu.chosen_ratio:.6f} motor encoder counts**")
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}**")
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):
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}"
)
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}. "
"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),
)
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,
)
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,
)
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).",
)
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,
)
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}")
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.")
elif page == "Parameter Guide":
st.title("🛠️ Essential Parameter Setup")
st.write("Beyond PUU (P1-44/45), these are high-impact parameters for gearbox systems.")
st.subheader("1. The 'Big Three' for Gearbox Safety")
st.markdown(
"""
| Parameter | Name | Recommended Value | Why? |
| :--- | :--- | :--- | :--- |
| **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. |
"""
)
st.subheader("2. Speed & Torque Limits")
st.info("Gear reduction increases output torque and reduces output speed. Use limits to protect mechanics.")
st.markdown(
"""
- **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.
"""
)
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}")
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()
)
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.
"""
)
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)
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
### Recommended workflow
- Compute PUU in **PUU Calculator**
- Set safe limits (**P1-02**, **P1-12**, **P1-13**)
- Verify motion direction and scaling
- Tune (**P1-37** and tuning flow)
"""
)
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).")