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 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,
@ -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,11 +598,15 @@ 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(
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.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.",
)
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),