added thousands notation and fixing latex

This commit is contained in:
Dejan Rožič 2026-01-07 08:07:03 +01:00
parent bd51824bd5
commit 9ccf422c4a

253
main.py
View file

@ -9,12 +9,14 @@ import math
from dataclasses import dataclass from dataclasses import dataclass
from datetime import datetime from datetime import datetime
from math import gcd from math import gcd
import streamlit as st import streamlit as st
# Optional PDF export (reportlab is installed in your environment) # Optional PDF export (reportlab is installed in your environment)
try: try:
from reportlab.lib.pagesizes import A4 from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas from reportlab.pdfgen import canvas
REPORTLAB_OK = True REPORTLAB_OK = True
except Exception: except Exception:
REPORTLAB_OK = False REPORTLAB_OK = False
@ -73,10 +75,109 @@ def clamp_float(x: float, lo: float, hi: float) -> float:
return max(lo, min(hi, x)) 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: def build_markdown_report(data: dict) -> str:
ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S") ts = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
lines = [] lines = []
lines.append(f"# Delta ASDA-A2 Commissioning Report") lines.append("# Delta ASDA-A2 Commissioning Report")
lines.append("") lines.append("")
lines.append(f"- Generated: **{ts}**") lines.append(f"- Generated: **{ts}**")
lines.append("") lines.append("")
@ -144,7 +245,6 @@ def markdown_to_pdf_bytes(title: str, md_text: str) -> bytes:
y = height - margin y = height - margin
for raw in md_text.splitlines(): for raw in md_text.splitlines():
# simple wrap
line = raw.rstrip("\n") line = raw.rstrip("\n")
if not line: if not line:
y -= line_height y -= line_height
@ -152,7 +252,7 @@ def markdown_to_pdf_bytes(title: str, md_text: str) -> bytes:
new_page() new_page()
continue 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: for chunk in chunks:
c.drawString(margin, y, chunk) c.drawString(margin, y, chunk)
y -= line_height 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") st.set_page_config(page_title="Delta ASDA-A2 Engineering Tool", layout="wide")
# default session values
defaults = { defaults = {
"motor_counts_per_rev": 128000.0, "motor_counts_per_rev": 128000.0,
"gear_ratio": 50.0, "gear_ratio": 50.0,
@ -379,44 +478,56 @@ page = st.sidebar.radio(
], ],
) )
# Global reset
with st.sidebar: with st.sidebar:
if st.button("Reset all inputs"): if st.button("Reset all inputs"):
# reset numeric values
for k, v in defaults.items(): for k, v in defaults.items():
st.session_state[k] = v 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() st.rerun()
# ---------------------------- # ----------------------------
# Pages # Pages
# ---------------------------- # ----------------------------
if page == "Theory & Architecture": if page == "Theory & Architecture":
st.title("📚 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( st.markdown(
r""" "- Motor counts per **load revolution** = (motor counts / motor rev) × (gear ratio)\n"
This tool helps you set up **electronic gearing (PUU / P1-44 & P1-45)** and speed/torque safety checks for gearbox systems. "- 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 st.markdown("### What “smoothness” means (practically)")
\[ st.write(
\text{Motor encoder counts} = \text{Command pulses} \times \frac{P1-44}{P1-45} "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."
)
### With a gearbox st.info(
- Motor counts per **load revolution** = (motor counts / motor rev) × (gear ratio) "Tip: If you command in **degrees**, 360 units/rev is a clean choice. "
- Choose user units per load revolution (e.g. **360 for degrees**) "If ratio becomes too large/small, scale units (e.g., 0.1° or 0.01°)."
\[
\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.
"""
) )
elif page == "PUU Calculator": elif page == "PUU Calculator":
st.title("🔢 Universal PUU Calculator (P1-44 / P1-45)") st.title("🔢 Universal PUU Calculator (P1-44 / P1-45)")
st.caption( st.caption(
"Compute electronic gearing for Delta ASDA-A2. Includes presets, exact vs drive-friendly fraction, and validations." "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: with col1:
st.subheader("Input") 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", "Motor encoder counts per motor revolution",
min_value=1.0, key="motor_counts_per_rev",
value=float(st.session_state.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).", 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( st.session_state.gear_ratio = st.number_input(
"Gear ratio (motor rev per load rev)", "Gear ratio (motor rev per load rev)",
min_value=0.0001, min_value=0.0001,
@ -483,11 +598,15 @@ elif page == "PUU Calculator":
) )
st.session_state.units_per_load_rev = float(st.session_state.pulley_circum_mm) st.session_state.units_per_load_rev = float(st.session_state.pulley_circum_mm)
else: else:
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", "User units per load revolution",
min_value=0.0001, key="units_per_load_rev",
value=float(st.session_state.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.controller_pulses_per_unit = st.number_input( st.session_state.controller_pulses_per_unit = st.number_input(
@ -498,11 +617,14 @@ elif page == "PUU Calculator":
help="If 1 pulse = 1 user unit, keep 1. If 10 pulses per degree, enter 10.", 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)", "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, min_value=1000,
value=int(st.session_state.ratio_limit),
step=1000,
) )
with st.expander("Delta A2 ratio limits (editable)"): with st.expander("Delta A2 ratio limits (editable)"):
@ -530,24 +652,24 @@ elif page == "PUU Calculator":
) )
# Compute PUU # Compute PUU
motor_counts_per_load_rev = st.session_state.motor_counts_per_rev * st.session_state.gear_ratio motor_counts_per_load_rev = float(st.session_state.motor_counts_per_rev) * float(st.session_state.gear_ratio)
denom_units = st.session_state.units_per_load_rev * st.session_state.controller_pulses_per_unit 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 target_ratio = motor_counts_per_load_rev / denom_units
try: try:
puu = compute_puu( puu = compute_puu(
motor_counts_per_rev=st.session_state.motor_counts_per_rev, motor_counts_per_rev=float(st.session_state.motor_counts_per_rev),
gear_ratio=st.session_state.gear_ratio, gear_ratio=float(st.session_state.gear_ratio),
units_per_load_rev=st.session_state.units_per_load_rev, units_per_load_rev=float(st.session_state.units_per_load_rev),
controller_pulses_per_unit=st.session_state.controller_pulses_per_unit, controller_pulses_per_unit=float(st.session_state.controller_pulses_per_unit),
ratio_limit=int(st.session_state.ratio_limit), ratio_limit=int(st.session_state.ratio_limit),
) )
except Exception as e: except Exception as e:
st.error(f"PUU computation error: {e}") st.error(f"PUU computation error: {e}")
st.stop() st.stop()
counts_per_user_unit_load = motor_counts_per_load_rev / st.session_state.units_per_load_rev counts_per_user_unit_load = motor_counts_per_load_rev / float(st.session_state.units_per_load_rev)
units_per_motor_rev = st.session_state.units_per_load_rev / st.session_state.gear_ratio units_per_motor_rev = float(st.session_state.units_per_load_rev) / float(st.session_state.gear_ratio)
validations = validate_delta_limits( validations = validate_delta_limits(
ratio=puu.chosen_ratio, ratio=puu.chosen_ratio,
@ -561,7 +683,6 @@ elif page == "PUU Calculator":
with col2: with col2:
st.subheader("Result") st.subheader("Result")
# Headline metrics
m1, m2, m3 = st.columns(3) m1, m2, m3 = st.columns(3)
m1.metric("P1-44 (N)", f"{puu.n_final}") m1.metric("P1-44 (N)", f"{puu.n_final}")
m2.metric("P1-45 (M)", f"{puu.d_final}") m2.metric("P1-45 (M)", f"{puu.d_final}")
@ -579,17 +700,17 @@ elif page == "PUU Calculator":
st.markdown("### Sanity checks") st.markdown("### Sanity checks")
st.write(f"- Motor counts per **load rev**: **{motor_counts_per_load_rev:.3f}**") 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"- 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}**") 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( 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: else:
st.warning( 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)." "Consider increasing counts per unit (e.g., change pulses/unit, change unit scaling)."
) )
@ -602,8 +723,11 @@ elif page == "PUU Calculator":
else: else:
st.success(v) st.success(v)
# Suggestions if out-of-range sugg = 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) puu.chosen_ratio,
float(st.session_state.delta_min_ratio),
float(st.session_state.delta_max_ratio),
)
if sugg: if sugg:
st.markdown("### Fix suggestions") st.markdown("### Fix suggestions")
for s in sugg: for s in sugg:
@ -611,7 +735,6 @@ elif page == "PUU Calculator":
st.divider() st.divider()
# Copy-to-clipboard (simple HTML button)
st.markdown("### Quick copy") st.markdown("### Quick copy")
txt = f"P1-44={puu.n_final}, P1-45={puu.d_final}" txt = f"P1-44={puu.n_final}, P1-45={puu.d_final}"
st.code(txt) st.code(txt)
@ -627,7 +750,6 @@ elif page == "PUU Calculator":
elif page == "Speed Translator": elif page == "Speed Translator":
st.title("⚡ Speed Translator (Motor ↔ Gearbox ↔ Load ↔ User Units)") 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.") st.caption("Convert between motor RPM, load RPM, and user units/s based on gearbox and unit definition.")
col1, col2 = st.columns(2) col1, col2 = st.columns(2)
@ -642,11 +764,14 @@ elif page == "Speed Translator":
step=1.0, 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", "User units per load revolution",
min_value=0.0001, key="units_per_load_rev",
value=float(st.session_state.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( 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"- Load RPS: **{speed['Load RPS']:.4f}**")
st.write(f"- User units/min: **{speed['User units / min']:.3f}**") st.write(f"- User units/min: **{speed['User units / min']:.3f}**")
if speed["Motor RPM"] > st.session_state.motor_rated_rpm: if speed["Motor RPM"] > float(st.session_state.motor_rated_rpm):
st.error( st.error(f"❌ Motor RPM exceeds rated RPM: {speed['Motor RPM']:.1f} > {float(st.session_state.motor_rated_rpm):.1f}")
f"❌ Motor RPM exceeds rated RPM: {speed['Motor RPM']:.1f} > {st.session_state.motor_rated_rpm:.1f}"
)
else: else:
st.success("✅ Motor RPM within rated RPM.") st.success("✅ Motor RPM within rated RPM.")
elif page == "Torque Estimator": elif page == "Torque Estimator":
st.title("🧲 Torque Estimator (Gearbox Amplification)") st.title("🧲 Torque Estimator (Gearbox Amplification)")
st.caption("Quick estimator: output torque ≈ motor torque × gear ratio × efficiency.") st.caption("Quick estimator: output torque ≈ motor torque × gear ratio × efficiency.")
col1, col2 = st.columns(2) 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}") 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}") m2.metric("Output peak torque (Nm)", f"{tor['Estimated output peak torque (Nm)']:.2f}")
st.info( st.info("Safety tip: during first tests, limit torque (P1-12/P1-13) because gearbox output torque can be huge.")
"Safety tip: during first tests, limit torque (P1-12/P1-13) because gearbox output torque can be huge."
)
elif page == "Parameter Guide": elif page == "Parameter Guide":
st.title("🛠️ Essential Parameter Setup") st.title("🛠️ Essential Parameter Setup")
@ -774,9 +894,7 @@ elif page == "Parameter Guide":
) )
st.subheader("2. Speed & Torque Limits") st.subheader("2. Speed & Torque Limits")
st.info( st.info("Gear reduction increases output torque and reduces output speed. Use limits to protect mechanics.")
"Gear reduction increases output torque and reduces output speed. Use limits to protect mechanics."
)
st.markdown( st.markdown(
""" """
- **P1-02 (Speed Limit):** Set to motor-rated speed or your machine limit. - **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. 2. Set value to 10.
3. Press 'SET'. 3. Press 'SET'.
4. Power Cycle (Off/On). 4. Power Cycle (Off/On).
""".strip(), """.strip()
language="markdown",
) )
with col_b: with col_b:
@ -822,7 +939,6 @@ elif page == "Parameter Guide":
elif page == "Commissioning Checklist": elif page == "Commissioning Checklist":
st.title("✅ Commissioning Checklist (Field Workflow)") st.title("✅ Commissioning Checklist (Field Workflow)")
st.caption("A practical sequence to minimize mistakes and avoid mechanical damage.") st.caption("A practical sequence to minimize mistakes and avoid mechanical damage.")
steps = [ steps = [
@ -869,7 +985,6 @@ elif page == "Export Report":
st.title("🧾 Export Commissioning Report (Markdown / PDF)") st.title("🧾 Export Commissioning Report (Markdown / PDF)")
st.caption("Generates a report of your current inputs, PUU result, checks, speed and torque estimates.") st.caption("Generates a report of your current inputs, PUU result, checks, speed and torque estimates.")
# Recompute PUU + checks for report
puu = compute_puu( puu = compute_puu(
motor_counts_per_rev=float(st.session_state.motor_counts_per_rev), motor_counts_per_rev=float(st.session_state.motor_counts_per_rev),
gear_ratio=float(st.session_state.gear_ratio), gear_ratio=float(st.session_state.gear_ratio),