added thousands notation and fixing latex
This commit is contained in:
parent
bd51824bd5
commit
9ccf422c4a
263
main.py
263
main.py
|
|
@ -9,12 +9,14 @@ 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
|
||||
|
|
@ -73,10 +75,109 @@ 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(f"# Delta ASDA-A2 Commissioning Report")
|
||||
lines.append("# Delta ASDA-A2 Commissioning Report")
|
||||
lines.append("")
|
||||
lines.append(f"- Generated: **{ts}**")
|
||||
lines.append("")
|
||||
|
|
@ -144,7 +245,6 @@ def markdown_to_pdf_bytes(title: str, md_text: str) -> bytes:
|
|||
y = height - margin
|
||||
|
||||
for raw in md_text.splitlines():
|
||||
# simple wrap
|
||||
line = raw.rstrip("\n")
|
||||
if not line:
|
||||
y -= line_height
|
||||
|
|
@ -152,7 +252,7 @@ def markdown_to_pdf_bytes(title: str, md_text: str) -> bytes:
|
|||
new_page()
|
||||
continue
|
||||
|
||||
chunks = [line[i:i + max_width_chars] for i in range(0, len(line), max_width_chars)]
|
||||
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
|
||||
|
|
@ -340,7 +440,6 @@ def template_blocks() -> dict[str, list[str]]:
|
|||
# ----------------------------
|
||||
st.set_page_config(page_title="Delta ASDA-A2 Engineering Tool", layout="wide")
|
||||
|
||||
# default session values
|
||||
defaults = {
|
||||
"motor_counts_per_rev": 128000.0,
|
||||
"gear_ratio": 50.0,
|
||||
|
|
@ -350,8 +449,8 @@ defaults = {
|
|||
"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
|
||||
"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,
|
||||
|
|
@ -379,44 +478,56 @@ page = st.sidebar.radio(
|
|||
],
|
||||
)
|
||||
|
||||
# Global reset
|
||||
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(
|
||||
r"""
|
||||
This tool helps you set up **electronic gearing (PUU / P1-44 & P1-45)** and speed/torque safety checks for gearbox systems.
|
||||
"- 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}}"
|
||||
)
|
||||
|
||||
### Core equation
|
||||
\[
|
||||
\text{Motor encoder counts} = \text{Command pulses} \times \frac{P1-44}{P1-45}
|
||||
\]
|
||||
|
||||
### With a gearbox
|
||||
- Motor counts per **load revolution** = (motor counts / motor rev) × (gear ratio)
|
||||
- Choose user units per load revolution (e.g. **360 for degrees**)
|
||||
|
||||
\[
|
||||
\frac{P1-44}{P1-45} = \frac{\text{motor counts per load rev}}{\text{units per load rev}\times\text{controller pulses per unit}}
|
||||
\]
|
||||
|
||||
### What “smoothness” really means
|
||||
A good rule of thumb is keeping **counts per user unit** high enough to avoid coarse stepping at low speed.
|
||||
This app shows **counts per user unit** and warns you if it drops below your chosen minimum.
|
||||
"""
|
||||
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."
|
||||
)
|
||||
|
|
@ -426,14 +537,18 @@ elif page == "PUU Calculator":
|
|||
with col1:
|
||||
st.subheader("Input")
|
||||
|
||||
st.session_state.motor_counts_per_rev = st.number_input(
|
||||
# BIG numbers => SI formatting (360.000 style)
|
||||
st.session_state.motor_counts_per_rev = si_number_input(
|
||||
"Motor encoder counts per motor revolution",
|
||||
min_value=1.0,
|
||||
key="motor_counts_per_rev",
|
||||
value=float(st.session_state.motor_counts_per_rev),
|
||||
step=1000.0,
|
||||
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,
|
||||
|
|
@ -483,12 +598,16 @@ elif page == "PUU Calculator":
|
|||
)
|
||||
st.session_state.units_per_load_rev = float(st.session_state.pulley_circum_mm)
|
||||
else:
|
||||
st.session_state.units_per_load_rev = st.number_input(
|
||||
"User units per load revolution",
|
||||
min_value=0.0001,
|
||||
value=float(st.session_state.units_per_load_rev),
|
||||
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.controller_pulses_per_unit = st.number_input(
|
||||
"Controller command pulses per user unit",
|
||||
|
|
@ -498,11 +617,14 @@ elif page == "PUU Calculator":
|
|||
help="If 1 pulse = 1 user unit, keep 1. If 10 pulses per degree, enter 10.",
|
||||
)
|
||||
|
||||
st.session_state.ratio_limit = st.number_input(
|
||||
# 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,
|
||||
value=int(st.session_state.ratio_limit),
|
||||
step=1000,
|
||||
)
|
||||
|
||||
with st.expander("Delta A2 ratio limits (editable)"):
|
||||
|
|
@ -530,24 +652,24 @@ elif page == "PUU Calculator":
|
|||
)
|
||||
|
||||
# Compute PUU
|
||||
motor_counts_per_load_rev = st.session_state.motor_counts_per_rev * st.session_state.gear_ratio
|
||||
denom_units = st.session_state.units_per_load_rev * st.session_state.controller_pulses_per_unit
|
||||
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=st.session_state.motor_counts_per_rev,
|
||||
gear_ratio=st.session_state.gear_ratio,
|
||||
units_per_load_rev=st.session_state.units_per_load_rev,
|
||||
controller_pulses_per_unit=st.session_state.controller_pulses_per_unit,
|
||||
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 / st.session_state.units_per_load_rev
|
||||
units_per_motor_rev = st.session_state.units_per_load_rev / 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,
|
||||
|
|
@ -561,7 +683,6 @@ elif page == "PUU Calculator":
|
|||
with col2:
|
||||
st.subheader("Result")
|
||||
|
||||
# Headline metrics
|
||||
m1, m2, m3 = st.columns(3)
|
||||
m1.metric("P1-44 (N)", f"{puu.n_final}")
|
||||
m2.metric("P1-45 (M)", f"{puu.d_final}")
|
||||
|
|
@ -579,17 +700,17 @@ elif page == "PUU Calculator":
|
|||
|
||||
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**: **{st.session_state.units_per_load_rev:.6f}**")
|
||||
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 >= st.session_state.smooth_min_counts_per_user_unit:
|
||||
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} ≥ {st.session_state.smooth_min_counts_per_user_unit:.1f}"
|
||||
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} < {st.session_state.smooth_min_counts_per_user_unit:.1f}. "
|
||||
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)."
|
||||
)
|
||||
|
||||
|
|
@ -602,8 +723,11 @@ elif page == "PUU Calculator":
|
|||
else:
|
||||
st.success(v)
|
||||
|
||||
# Suggestions if out-of-range
|
||||
sugg = suggestions_if_out_of_range(puu.chosen_ratio, st.session_state.delta_min_ratio, st.session_state.delta_max_ratio)
|
||||
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:
|
||||
|
|
@ -611,7 +735,6 @@ elif page == "PUU Calculator":
|
|||
|
||||
st.divider()
|
||||
|
||||
# Copy-to-clipboard (simple HTML button)
|
||||
st.markdown("### Quick copy")
|
||||
txt = f"P1-44={puu.n_final}, P1-45={puu.d_final}"
|
||||
st.code(txt)
|
||||
|
|
@ -627,7 +750,6 @@ elif page == "PUU Calculator":
|
|||
|
||||
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)
|
||||
|
|
@ -642,11 +764,14 @@ elif page == "Speed Translator":
|
|||
step=1.0,
|
||||
)
|
||||
|
||||
st.session_state.units_per_load_rev = st.number_input(
|
||||
st.session_state.units_per_load_rev = si_number_input(
|
||||
"User units per load revolution",
|
||||
min_value=0.0001,
|
||||
key="units_per_load_rev",
|
||||
value=float(st.session_state.units_per_load_rev),
|
||||
step=1.0,
|
||||
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(
|
||||
|
|
@ -699,16 +824,13 @@ elif page == "Speed Translator":
|
|||
st.write(f"- Load RPS: **{speed['Load RPS']:.4f}**")
|
||||
st.write(f"- User units/min: **{speed['User units / min']:.3f}**")
|
||||
|
||||
if speed["Motor RPM"] > st.session_state.motor_rated_rpm:
|
||||
st.error(
|
||||
f"❌ Motor RPM exceeds rated RPM: {speed['Motor RPM']:.1f} > {st.session_state.motor_rated_rpm:.1f}"
|
||||
)
|
||||
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)
|
||||
|
|
@ -754,9 +876,7 @@ elif page == "Torque Estimator":
|
|||
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."
|
||||
)
|
||||
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")
|
||||
|
|
@ -774,9 +894,7 @@ elif page == "Parameter Guide":
|
|||
)
|
||||
|
||||
st.subheader("2. Speed & Torque Limits")
|
||||
st.info(
|
||||
"Gear reduction increases output torque and reduces output speed. Use limits to protect mechanics."
|
||||
)
|
||||
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.
|
||||
|
|
@ -805,8 +923,7 @@ elif page == "Parameter Guide":
|
|||
2. Set value to 10.
|
||||
3. Press 'SET'.
|
||||
4. Power Cycle (Off/On).
|
||||
""".strip(),
|
||||
language="markdown",
|
||||
""".strip()
|
||||
)
|
||||
|
||||
with col_b:
|
||||
|
|
@ -822,7 +939,6 @@ elif page == "Parameter Guide":
|
|||
|
||||
elif page == "Commissioning Checklist":
|
||||
st.title("✅ Commissioning Checklist (Field Workflow)")
|
||||
|
||||
st.caption("A practical sequence to minimize mistakes and avoid mechanical damage.")
|
||||
|
||||
steps = [
|
||||
|
|
@ -869,7 +985,6 @@ 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.")
|
||||
|
||||
# Recompute PUU + checks for report
|
||||
puu = compute_puu(
|
||||
motor_counts_per_rev=float(st.session_state.motor_counts_per_rev),
|
||||
gear_ratio=float(st.session_state.gear_ratio),
|
||||
|
|
|
|||
Loading…
Reference in a new issue