From 9ccf422c4a9fa095bf3d3a0b2746d2c525a805ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dejan=20Ro=C5=BEi=C4=8D?= Date: Wed, 7 Jan 2026 08:07:03 +0100 Subject: [PATCH] added thousands notation and fixing latex --- main.py | 263 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 189 insertions(+), 74 deletions(-) diff --git a/main.py b/main.py index 313285f..de86b92 100644 --- a/main.py +++ b/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),